| Type | Name | Status | PK | FK | Columns | Index | Script | Diff Script |
|---|---|---|---|---|---|---|---|---|
| Table | company_defaultor_configurations | Missing in Target |
|
|||||
| Table | draft_invoice_details | Missing in Target |
|
|||||
| Table | draft_invoice_header_ids | Missing in Target |
|
|||||
| Table | invoice_approval_user_company | Missing in Target |
|
|||||
| Table | invoice_payment_header_ids | Missing in Target |
|
|||||
| Table | invoice_penalties | Missing in Target |
|
|||||
| Table | invoice_status_company_configs | Missing in Target |
|
|||||
| Table | invoice_header_ids | Missing in Target |
|
|||||
| Table | penalty_processing_logs | Missing in Target |
|
|||||
| Table | schedule_execution_logs | Missing in Target |
|
|||||
| Table | customer_note_statuses | Missing in Target |
|
|||||
| Table | customer_upis | Missing in Target |
|
|||||
| Table | record_exists | Missing in Target |
|
|||||
| Table | temp_invoice_next_status | Missing in Target |
|
|||||
| Table | invoice_statuses | Missing in Target |
|
|||||
| Table | invoice_voucher_ids | Missing in Target |
|
|||||
| Table | customers | Missing in Target |
|
|||||
| Table | draft_invoice_headers | Missing in Target |
|
|||||
| Table | organizations | Missing in Target |
|
|||||
| Table | schema_versions | Missing in Target |
|
|||||
| Table | audit_queue_invoice_payment_headers | Missing in Target |
|
|||||
| Table | upis | Missing in Target |
|
|||||
| Table | users | Missing in Target |
|
|||||
| Table | v_created_by | Missing in Target |
|
|||||
| Table | v_invoice_status_id | Missing in Target |
|
|||||
| Table | warehouses | Missing in Target |
|
|||||
| Table | invoice_approval_users_account | Missing in Target |
|
|||||
| Table | invoice_approval_logs | Missing in Target |
|
|||||
| Table | addresses | Missing in Target |
|
|||||
| Table | audit_queue_invoice_details | Missing in Target |
|
|||||
| Table | customer_contacts | Missing in Target |
|
|||||
| Table | customer_note_headers | Missing in Target |
|
|||||
| Table | customers_audit | Missing in Target |
|
|||||
| Table | gate_pass_details | Missing in Target |
|
|||||
| Table | penalty_frequencies | Missing in Target |
|
|||||
| Table | payment_statuses | Missing in Target |
|
|||||
| Table | group_invoice_headers | Missing in Target |
|
|||||
| Table | invoice_account_approval_levels | Missing in Target |
|
|||||
| Table | invoice_workflow | Missing in Target |
|
|||||
| Table | invoice_payment_details | Missing in Target |
|
|||||
| Table | penalty_configs | Missing in Target |
|
|||||
| Table | roles | Missing in Target |
|
|||||
| Table | states | Missing in Target |
|
|||||
| Table | user_roles | Missing in Target |
|
|||||
| Table | audit_queue_invoice_headers | Missing in Target |
|
|||||
| Table | recurring_sales_schedules | Missing in Target |
|
|||||
| Table | invoice_payment_headers | Missing in Target |
|
|||||
| Table | invoice_headers | Missing in Target |
|
|||||
| Table | audit_queue | Missing in Target |
|
|||||
| Table | customer_bank_accounts | Missing in Target |
|
|||||
| Table | bank_accounts | Missing in Target |
|
|||||
| Table | contacts | Missing in Target |
|
|||||
| Table | batch_schedules | Missing in Target |
|
|||||
| Table | __EFMigrationsHistory | Missing in Target |
|
|||||
| Table | cities | Missing in Target |
|
|||||
| Table | countries | Missing in Target |
|
|||||
| Table | banks | Missing in Target |
|
|||||
| Table | companies | Missing in Target |
|
|||||
| Table | customer_default_accounts | Missing in Target |
|
|||||
| Table | customer_note_details | Missing in Target |
|
|||||
| Table | customer_note_work_flows | Missing in Target |
|
|||||
| Table | gate_pass_headers | Missing in Target |
|
|||||
| Table | gate_pass_statuses | Missing in Target |
|
|||||
| Table | delinquency_cycles | Missing in Target |
|
|||||
| Table | delinquency_snapshots | Missing in Target |
|
|||||
| Table | collection_policies | Missing in Target |
|
|||||
| Table | delinquency_discussion_messages | Missing in Target |
|
|||||
| Table | group_invoice_header_ids | Missing in Target |
|
|||||
| Table | group_invoice_details | Missing in Target |
|
|||||
| Table | group_invoice_template_customers | Missing in Target |
|
|||||
| Table | v_user_id | Missing in Target |
|
|||||
| Table | delinquency_actions | Missing in Target |
|
|||||
| Table | delinquency_resolution_window | Missing in Target |
|
|||||
| Table | delinquency_snapshot_invoices | Missing in Target |
|
|||||
| Table | group_invoice_templates | Missing in Target |
|
|||||
| Table | invoice_approval_issue_log | Missing in Target |
|
|||||
| Table | invoice_details | Missing in Target |
|
|||||
| Table | recurrence_schedule_details | Missing in Target |
|
|||||
| Table | audit_queue_invoice_payment_details | Missing in Target |
|
|||||
| Table | delinquency_action_types | Missing in Target |
|
|||||
| Table | delinquency_resolution_states | Missing in Target |
|
|||||
| Table | v_contact_number | Missing in Target |
|
|||||
| Function | check_invoice_approval_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.check_invoice_approval_permissions(p_company_id uuid, p_status_id integer, p_user_id uuid, p_account_id uuid, p_approval_level integer)
2
RETURNS TABLE(company_id uuid, status_id integer, user_id uuid, approval_level integer, account_id uuid, account_status_id integer, permission_check text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_has_permission BOOLEAN;
7
BEGIN
8
-- First check for account-level permissions
9
RETURN QUERY
10
SELECT
11
iaua.company_id,
12
iaua.status_id,
13
iaua.user_id,
14
iaua.approval_level,
15
iaua.account_id,
16
iaua.status_id AS account_status_id,
17
'Account-level permission granted' AS permission_check
18
FROM public.invoice_approval_users_account iaua
19
WHERE iaua.company_id = p_company_id
20
AND iaua.status_id = p_status_id
21
AND iaua.user_id = p_user_id
22
AND iaua.account_id = p_account_id -- Account-level permissions check
23
AND iaua.approval_level = p_approval_level -- Checking the approval level
24
AND iaua.is_deleted = false;
25
26
-- If no rows were found in the account-level check, proceed to check for company-level permissions
27
IF NOT FOUND THEN
28
RETURN QUERY
29
SELECT
30
iac.company_id,
31
iac.status_id,
32
iac.user_id,
33
iac.approval_level,
34
NULL::uuid AS account_id, -- No account-level ID
35
iac.status_id AS account_status_id,
36
'Company-level permission granted' AS permission_check
37
FROM public.invoice_approval_user_company iac
38
WHERE iac.company_id = p_company_id
39
AND iac.status_id = p_status_id
40
AND iac.user_id = p_user_id
41
AND iac.approval_level = p_approval_level -- Checking the approval level
42
AND iac.is_deleted = false;
43
END IF;
44
END;
45
$function$
|
|||||
| Function | delete_bulk_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.delete_bulk_invoices(p_invoice_ids uuid[])
2
RETURNS TABLE(id integer, invoice_id uuid, status text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_invoice_id UUID;
7
v_status_id INT;
8
v_now TIMESTAMP := now();
9
row_index INT := 1;
10
BEGIN
11
FOREACH v_invoice_id IN ARRAY p_invoice_ids LOOP
12
-- Final Invoice check
13
SELECT ih.invoice_status_id INTO v_status_id
14
FROM invoice_headers ih
15
WHERE ih.id = v_invoice_id AND ih.is_deleted = false;
16
17
IF FOUND THEN
18
IF v_status_id IN (3, 4, 5) THEN
19
id := row_index;
20
invoice_id := v_invoice_id;
21
status := 'Blocked - Final Invoice Status is Approved/Paid/Partially Paid';
22
row_index := row_index + 1;
23
RETURN NEXT;
24
CONTINUE;
25
END IF;
26
27
-- Soft delete final invoice
28
UPDATE invoice_details idt
29
SET is_deleted = true,
30
deleted_on_utc = v_now
31
WHERE idt.invoice_header_id = v_invoice_id;
32
33
UPDATE invoice_headers ih
34
SET is_deleted = true,
35
deleted_on_utc = v_now
36
WHERE ih.id = v_invoice_id;
37
38
id := row_index;
39
invoice_id := v_invoice_id;
40
status := 'Deleted - Final Invoice';
41
row_index := row_index + 1;
42
RETURN NEXT;
43
CONTINUE;
44
END IF;
45
46
-- Draft Invoice check
47
SELECT dih.invoice_status_id INTO v_status_id
48
FROM draft_invoice_headers dih
49
WHERE dih.id = v_invoice_id AND dih.is_deleted = false;
50
51
IF FOUND THEN
52
IF v_status_id IN (3, 4, 5) THEN
53
id := row_index;
54
invoice_id := v_invoice_id;
55
status := 'Blocked - Draft Invoice Status is Approved/Paid/Partially Paid';
56
row_index := row_index + 1;
57
RETURN NEXT;
58
CONTINUE;
59
END IF;
60
61
-- Soft delete draft invoice
62
UPDATE draft_invoice_details did
63
SET is_deleted = true,
64
deleted_on_utc = v_now
65
WHERE did.invoice_header_id = v_invoice_id;
66
67
UPDATE draft_invoice_headers dih
68
SET is_deleted = true,
69
deleted_on_utc = v_now
70
WHERE dih.id = v_invoice_id;
71
72
id := row_index;
73
invoice_id := v_invoice_id;
74
status := 'Deleted - Draft Invoice';
75
row_index := row_index + 1;
76
RETURN NEXT;
77
CONTINUE;
78
END IF;
79
80
-- Not found
81
id := row_index;
82
invoice_id := v_invoice_id;
83
status := 'Skipped - Invoice Not Found or Already Deleted';
84
row_index := row_index + 1;
85
RETURN NEXT;
86
END LOOP;
87
END;
88
$function$
|
|||||
| Function | generate_group_invoices_return_header_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.generate_group_invoices_return_header_ids(p_template_id uuid)
2
RETURNS uuid[]
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_ts timestamptz := now();
7
v_header_ids uuid[];
8
BEGIN
9
-- Call your existing (unchanged) procedure
10
CALL public.run_grouped_invoices(p_template_id);
11
12
-- Collect headers created in this invocation window
13
SELECT COALESCE(array_agg(h.id), '{}') INTO v_header_ids
14
FROM public.group_invoice_headers h
15
WHERE h.group_invoice_template_id = p_template_id
16
AND h.is_deleted = FALSE
17
AND h.created_on_utc >= v_start_ts; -- created_on_utc is already saved via NOW() in proc
18
19
RETURN v_header_ids;
20
END;
21
$function$
|
|||||
| Function | get_all_customers | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_customers(p_company_id uuid)
2
RETURNS TABLE(id uuid, name character varying, company_id uuid, billing_address_id uuid, billing_address_line1 text, billing_address_line2 text, billing_zip_code text, shipping_address_id uuid, shipping_address_line1 text, shipping_address_line2 text, shipping_zip_code text, gstin text, pan text, tan text, contacts jsonb, created_by uuid, modified_by uuid, created_by_user text, modified_by_user text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
c.id,
9
c.name,
10
c.company_id,
11
c.billing_address_id,
12
ba.address_line1 AS billing_address_line1,
13
ba.address_line2 AS billing_address_line2,
14
ba.zip_code AS billing_zip_code,
15
c.shipping_address_id,
16
sa.address_line1 AS shipping_address_line1,
17
sa.address_line2 AS shipping_address_line2,
18
sa.zip_code AS shipping_zip_code,
19
c.gstin,
20
c.pan,
21
c.tan,
22
-- JSON aggregation of contacts
23
COALESCE(jsonb_agg(
24
jsonb_build_object(
25
'contact_id', con.id,
26
'first_name', con.first_name,
27
'last_name', con.last_name,
28
'email', con.email,
29
'phone_number', con.phone_number,
30
'mobile_number', con.mobile_number,
31
'is_primary', con.is_primary
32
)
33
) FILTER (WHERE con.id IS NOT NULL), '[]'::jsonb) AS contacts,
34
c.created_by,
35
c.modified_by,
36
CONCAT(uc.first_name, ' ', uc.last_name) AS created_by_user,
37
CONCAT(um.first_name, ' ', um.last_name) AS modified_by_user
38
FROM
39
customers c
40
LEFT JOIN addresses ba ON c.billing_address_id = ba.id
41
LEFT JOIN addresses sa ON c.shipping_address_id = sa.id
42
LEFT JOIN customer_contacts cc ON c.id = cc.customer_id
43
LEFT JOIN contacts con ON cc.contact_id = con.id
44
LEFT JOIN users uc ON c.created_by = uc.id
45
LEFT JOIN users um ON c.modified_by = um.id
46
WHERE c.is_deleted = FALSE
47
AND (ba.is_deleted IS FALSE OR ba.is_deleted IS NULL)
48
AND (sa.is_deleted IS FALSE OR sa.is_deleted IS NULL)
49
AND (con.is_deleted IS FALSE OR con.is_deleted IS NULL)
50
AND c.company_id = p_company_id
51
GROUP BY c.id, ba.id, sa.id, uc.first_name, uc.last_name, um.first_name, um.last_name;
52
END;
53
$function$
|
|||||
| Function | get_executed_grouped_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_executed_grouped_invoice(p_company_id uuid)
2
RETURNS TABLE(id uuid, template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
executions.id,
9
executions.template_id,
10
executions.execution_date,
11
executions.error_message,
12
executions.success_count,
13
executions.total_count,
14
templates.invoice_description
15
FROM
16
public.apartment_invoice_template_executions executions
17
INNER JOIN
18
public.apartment_invoice_templates templates
19
ON
20
executions.template_id = templates.id
21
WHERE
22
executions.company_id = p_company_id
23
24
ORDER BY
25
executions.execution_date DESC;
26
END;
27
$function$
|
|||||
| Function | get_group_invoice_summary | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_group_invoice_summary(p_group_invoice_id uuid)
2
RETURNS TABLE(id uuid, group_invoice_number text, execution_date timestamp without time zone, invoice_description text, paid_count integer, paid_amount numeric, unpaid_count integer, unpaid_amount numeric, overdue_count integer, overdue_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
gih.id,
9
gih.group_invoice_number,
10
gih.execution_date,
11
git.invoice_description,
12
13
-- Paid
14
COALESCE(COUNT(CASE WHEN ih.invoice_status_id = 5 THEN 1 END), 0)::int AS paid_count,
15
COALESCE(SUM(CASE WHEN ih.invoice_status_id = 5 THEN ih.total_amount END), 0) AS paid_amount,
16
17
-- Unpaid
18
COALESCE(COUNT(CASE WHEN ih.invoice_status_id IN (1,2,3,4) THEN 1 END), 0)::int AS unpaid_count,
19
COALESCE(SUM(CASE WHEN ih.invoice_status_id IN (1,2,3,4) THEN ih.total_amount END), 0) AS unpaid_amount,
20
21
-- Overdue
22
COALESCE(COUNT(CASE WHEN ih.invoice_status_id NOT IN (5) AND ih.due_date < now() THEN 1 END), 0)::int AS overdue_count,
23
COALESCE(SUM(CASE WHEN ih.invoice_status_id NOT IN (5) AND ih.due_date < now() THEN ih.total_amount END), 0) AS overdue_amount
24
25
FROM group_invoice_headers gih
26
INNER JOIN group_invoice_templates git
27
ON gih.group_invoice_template_id = git.id
28
LEFT JOIN group_invoice_details gid
29
ON gid.group_invoice_header_id = gih.id AND gid.is_deleted = false
30
LEFT JOIN invoice_headers ih
31
ON ih.id = gid.invoice_header_id AND ih.is_deleted = false
32
WHERE gih.id = p_group_invoice_id
33
AND gih.is_deleted = false
34
GROUP BY gih.id, gih.group_invoice_number, gih.execution_date, git.invoice_description;
35
END;
36
$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
16
BEGIN
17
-- Get financial year details
18
SELECT fy.start_date, fy.end_date,
19
EXTRACT(YEAR FROM fy.start_date) INTO v_financial_year_start,
20
v_financial_year_end, v_finance_year_start
21
FROM public.finance_year fy
22
WHERE id = p_finance_id;
23
24
-- Set financial year end numeric value (next year)
25
v_finance_year_end := v_finance_year_start + 1;
26
27
-- Monthly Report
28
IF p_period_type = CONST_MONTHLY THEN
29
RETURN QUERY
30
WITH months AS (
31
SELECT
32
TO_CHAR(months.m, 'Mon YYYY') AS period,
33
EXTRACT(YEAR FROM months.m) AS extracted_year,
34
EXTRACT(MONTH FROM months.m) AS month_num
35
FROM
36
generate_series(v_financial_year_start, v_financial_year_end, '1 month'::interval) AS months(m)
37
)
38
SELECT
39
months.period,
40
months.extracted_year AS year,
41
-- Accrued Income & Expense
42
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(4)) THEN je.amount ELSE 0 END), 0) AS total_income,
43
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(5)) THEN je.amount ELSE 0 END), 0) AS total_expense,
44
-- Actual Income & Expense from Payments
45
COALESCE(SUM(CASE WHEN tr.transaction_source_type = 3 THEN je.amount ELSE 0 END), 0) AS actual_income, -- Invoice Payments
46
COALESCE(SUM(CASE
47
WHEN tr.transaction_source_type IN (4, 6)
48
AND at.id IN (8,9)
49
THEN ABS(je.amount)
50
ELSE 0 END), 0) AS actual_expense
51
52
-- Bill & Salary Payments
53
FROM
54
months
55
LEFT JOIN journal_entries je
56
ON EXTRACT(MONTH FROM je.transaction_date) = months.month_num
57
AND EXTRACT(YEAR FROM je.transaction_date) = months.extracted_year
58
AND je.is_deleted = FALSE
59
JOIN transaction_headers tr ON je.transaction_id = tr.id AND tr.company_id = p_company_id
60
JOIN chart_of_accounts coa ON je.account_id = coa.id
61
JOIN public.account_types at ON coa.account_type_id = at.id
62
GROUP BY months.period, months.extracted_year, months.month_num
63
ORDER BY months.extracted_year, months.month_num;
64
65
-- QUARTERLY REPORT
66
ELSEIF p_period_type = CONST_QUARTERLY THEN
67
RETURN QUERY
68
WITH quarters AS (
69
SELECT 'Q1' AS period, 4 AS start_month, 6 AS end_month, v_finance_year_start AS extracted_year
70
UNION ALL
71
SELECT 'Q2', 7, 9, v_finance_year_start
72
UNION ALL
73
SELECT 'Q3', 10, 12, v_finance_year_start
74
UNION ALL
75
SELECT 'Q4', 1, 3, v_finance_year_end
76
)
77
SELECT
78
CONCAT(quarters.period, ' ', quarters.extracted_year) AS period,
79
quarters.extracted_year AS year,
80
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(4)) THEN je.amount ELSE 0 END), 0) AS total_income,
81
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(5)) THEN je.amount ELSE 0 END), 0) AS total_expense,
82
COALESCE(SUM(CASE WHEN tr.transaction_source_type = 3 THEN je.amount ELSE 0 END), 0) AS actual_income,
83
COALESCE(SUM(CASE WHEN tr.transaction_source_type IN (4, 6) AND at.id IN (8,9) THEN ABS(je.amount) ELSE 0 END), 0) AS actual_expense
84
FROM
85
quarters
86
LEFT JOIN journal_entries je
87
ON EXTRACT(MONTH FROM je.transaction_date) BETWEEN quarters.start_month AND quarters.end_month
88
AND EXTRACT(YEAR FROM je.transaction_date) = quarters.extracted_year
89
AND je.is_deleted = FALSE
90
JOIN transaction_headers tr ON je.transaction_id = tr.id AND tr.company_id = p_company_id
91
JOIN chart_of_accounts coa ON je.account_id = coa.id
92
JOIN public.account_types at ON coa.account_type_id = at.id
93
GROUP BY quarters.period, quarters.extracted_year
94
ORDER BY quarters.extracted_year, quarters.period;
95
96
97
ELSEIF p_period_type = CONST_HALF_YEARLY THEN
98
RETURN QUERY
99
WITH half_years AS (
100
-- First Half: April to September (Same Financial Year)
101
SELECT 'H1' AS period, 4 AS start_month, 9 AS end_month, v_finance_year_start AS extracted_year
102
UNION ALL
103
-- Second Half: October to March (Spanning Next Year)
104
SELECT 'H2', 10, 12, v_finance_year_start
105
UNION ALL
106
SELECT 'H2', 1, 3, v_finance_year_end
107
)
108
SELECT
109
CONCAT(half_years.period, ' ', half_years.extracted_year) AS period,
110
half_years.extracted_year AS year,
111
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(4)) THEN je.amount ELSE 0 END), 0) AS total_income,
112
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(5)) THEN je.amount ELSE 0 END), 0) AS total_expense,
113
COALESCE(SUM(CASE WHEN tr.transaction_source_type = 3 THEN je.amount ELSE 0 END), 0) AS actual_income,
114
COALESCE(SUM(CASE WHEN tr.transaction_source_type IN (4, 6) AND at.id IN (8,9) THEN ABS(je.amount) ELSE 0 END), 0) AS actual_expense
115
FROM
116
half_years
117
LEFT JOIN journal_entries je
118
ON (
119
EXTRACT(MONTH FROM je.transaction_date) BETWEEN half_years.start_month AND half_years.end_month
120
AND EXTRACT(YEAR FROM je.transaction_date) = half_years.extracted_year
121
)
122
AND je.is_deleted = FALSE
123
JOIN transaction_headers tr ON je.transaction_id = tr.id AND tr.company_id = p_company_id
124
JOIN chart_of_accounts coa ON je.account_id = coa.id
125
JOIN public.account_types at ON coa.account_type_id = at.id
126
GROUP BY half_years.period, half_years.extracted_year
127
ORDER BY half_years.extracted_year, half_years.period;
128
129
-- Yearly Report
130
ELSIF p_period_type = CONST_YEARLY THEN
131
RETURN QUERY
132
SELECT
133
CONCAT('Year ', EXTRACT(YEAR FROM je.transaction_date)) AS period,
134
EXTRACT(YEAR FROM je.transaction_date) AS year,
135
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(4)) THEN je.amount ELSE 0 END), 0) AS total_income,
136
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(5)) THEN je.amount ELSE 0 END), 0) AS total_expense,
137
COALESCE(SUM(CASE WHEN tr.transaction_source_type = 3 THEN je.amount ELSE 0 END), 0) AS actual_income,
138
COALESCE(SUM(CASE WHEN tr.transaction_source_type IN (4, 6) AND at.id IN (8,9) THEN ABS(je.amount) ELSE 0 END), 0) AS actual_expense
139
FROM
140
journal_entries je
141
JOIN chart_of_accounts coa ON je.account_id = coa.id
142
JOIN public.account_types at ON coa.account_type_id = at.id
143
JOIN transaction_headers tr ON je.transaction_id = tr.id AND tr.company_id = p_company_id
144
WHERE
145
je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
146
AND je.is_deleted = FALSE
147
GROUP BY year
148
ORDER BY year;
149
150
END IF;
151
152
END;
153
$function$
|
|||||
| Function | get_invoice_approval_level | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_approval_level(p_company_id uuid, p_account_id uuid, p_status_id integer)
2
RETURNS integer
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_approval_level_required integer;
7
BEGIN
8
-- Try to get the approval level from invoice_account_approval_levels
9
SELECT approval_level_required
10
INTO v_approval_level_required
11
FROM public.invoice_account_approval_levels
12
WHERE company_id = p_company_id
13
AND account_id = p_account_id
14
AND status_id = p_status_id
15
AND is_deleted = false
16
LIMIT 1;
17
18
-- If found, return the approval level from invoice_account_approval_levels
19
IF FOUND THEN
20
RETURN v_approval_level_required;
21
END IF;
22
23
-- Fallback to invoice_workflow if no entry found in invoice_account_approval_levels
24
SELECT approval_level
25
INTO v_approval_level_required
26
FROM public.invoice_workflow
27
WHERE company_id = p_company_id
28
AND status = p_status_id
29
AND is_deleted = false
30
AND is_enabled = true
31
LIMIT 1;
32
33
-- Return the approval level from invoice_workflow
34
RETURN v_approval_level_required;
35
END
36
$function$
|
|||||
| Function | get_invoice_status | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_status(p_invoice_id uuid)
2
RETURNS integer
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
invoice_status_id INTEGER;
7
BEGIN
8
-- First, try to find the status in the draft_invoice_headers table
9
SELECT dih.invoice_status_id
10
INTO invoice_status_id
11
FROM public.draft_invoice_headers dih
12
WHERE dih.id = p_invoice_id;
13
14
-- If not found in draft_invoice_headers, check invoice_headers
15
IF invoice_status_id IS NULL THEN
16
SELECT ih.invoice_status_id
17
INTO invoice_status_id
18
FROM public.invoice_headers ih
19
WHERE ih.id = p_invoice_id;
20
END IF;
21
22
RETURN invoice_status_id;
23
END;
24
$function$
|
|||||
| Function | get_invoice_status_company_configs | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_status_company_configs(p_company_id uuid)
2
RETURNS TABLE(id integer, company_id uuid, status_id integer, status_name character varying, is_enabled boolean)
3
LANGUAGE sql
4
AS $function$
5
SELECT
6
c.id,
7
c.company_id,
8
c.status_id,
9
s.name AS status_name,
10
c.is_enabled
11
FROM public.invoice_status_company_configs c
12
JOIN public.invoice_statuses s ON s.id = c.status_id
13
WHERE c.company_id = p_company_id
14
AND s.is_deleted = false;
15
$function$
|
|||||
| Function | get_new_group_invoice_number | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_new_group_invoice_number(p_company_id uuid, p_date date)
2
RETURNS character varying
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_finance_year integer;
7
new_group_invoice_id integer;
8
p_prefix varchar;
9
group_invoice_number varchar;
10
BEGIN
11
-- Determine the start year of the financial year based on p_date
12
IF EXTRACT(MONTH FROM p_date) >= 4 THEN
13
v_finance_year := EXTRACT(YEAR FROM p_date);
14
ELSE
15
v_finance_year := EXTRACT(YEAR FROM p_date) - 1;
16
END IF;
17
18
-- Attempt to update the last_group_invoice_id for the given company and financial year
19
WITH x AS (
20
UPDATE public.group_invoice_header_ids
21
SET last_group_invoice_id = last_group_invoice_id + 1
22
WHERE company_id = p_company_id AND fin_year = v_finance_year
23
RETURNING last_group_invoice_id, group_invoice_prefix
24
),
25
insert_x AS (
26
-- If no rows were updated, insert a new row with the default prefix 'GIN' and default invoice_length
27
INSERT INTO public.group_invoice_header_ids (company_id, fin_year, group_invoice_prefix, last_group_invoice_id, group_invoice_length)
28
SELECT p_company_id, v_finance_year, 'GIN', 1, 8 -- Default invoice_length set to 8
29
WHERE NOT EXISTS (SELECT 1 FROM x)
30
RETURNING last_group_invoice_id, group_invoice_prefix
31
)
32
-- Use COALESCE to return the correct last_group_invoice_id and group_invoice_prefix
33
SELECT COALESCE((SELECT last_group_invoice_id FROM x LIMIT 1), (SELECT last_group_invoice_id FROM insert_x LIMIT 1)),
34
COALESCE((SELECT group_invoice_prefix FROM x LIMIT 1), (SELECT group_invoice_prefix FROM insert_x LIMIT 1))
35
INTO new_group_invoice_id, p_prefix;
36
37
-- Concatenate the prefix and new_group_invoice_id to form the group invoice number
38
group_invoice_number := p_prefix || LPAD(new_group_invoice_id::text, 4, '0');
39
40
-- Return the generated group invoice number
41
RETURN group_invoice_number;
42
END;
43
$function$
|
|||||
| Function | get_new_payment_number | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_new_payment_number(p_company_id uuid, p_date date)
2
RETURNS character varying
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_finance_year integer;
7
new_payment_id integer;
8
p_prefix varchar;
9
payment_number varchar;
10
BEGIN
11
-- Determine the start year of the financial year based on p_date
12
IF EXTRACT(MONTH FROM p_date) >= 4 THEN
13
v_finance_year := EXTRACT(YEAR FROM p_date);
14
ELSE
15
v_finance_year := EXTRACT(YEAR FROM p_date) - 1;
16
END IF;
17
18
-- Try to update and return existing row
19
WITH x AS (
20
UPDATE public.invoice_payment_header_ids
21
SET last_payment_id = last_payment_id + 1
22
WHERE company_id = p_company_id AND fin_year = v_finance_year
23
RETURNING last_payment_id, payment_prefix
24
),
25
insert_x AS (
26
-- If not updated, insert a default row
27
INSERT INTO public.invoice_payment_header_ids (
28
company_id, fin_year, payment_prefix, last_payment_id, payment_length
29
)
30
SELECT p_company_id, v_finance_year, 'PAY', 1, 8
31
WHERE NOT EXISTS (SELECT 1 FROM x)
32
RETURNING last_payment_id, payment_prefix
33
)
34
-- Retrieve from update or insert result
35
SELECT
36
COALESCE((SELECT last_payment_id FROM x LIMIT 1), (SELECT last_payment_id FROM insert_x LIMIT 1)),
37
COALESCE((SELECT payment_prefix FROM x LIMIT 1), (SELECT payment_prefix FROM insert_x LIMIT 1))
38
INTO new_payment_id, p_prefix;
39
40
-- Construct the final payment number (e.g., PAY0001)
41
payment_number := p_prefix || LPAD(new_payment_id::text, 4, '0');
42
43
RETURN payment_number;
44
END;
45
$function$
|
|||||
| Function | get_payment_distributions_for_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_payment_distributions_for_invoice(p_invoice_header_id uuid)
2
RETURNS TABLE(payment_detail_id uuid, invoice_payment_header_id uuid, payment_number text, invoice_header_id uuid, received_amount numeric, payment_created_by uuid, payment_created_by_name text, payment_created_on timestamp without time zone)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
ipd.id AS payment_detail_id,
9
ipd.invoice_payment_header_id,
10
iph.payment_number,
11
ipd.invoice_header_id,
12
ipd.received_amount,
13
ipd.created_by AS payment_created_by,
14
CONCAT(u.first_name, ' ', u.last_name) AS payment_created_by_name,
15
ipd.created_on_utc AS payment_created_on
16
FROM
17
public.invoice_payment_details ipd
18
JOIN public.invoice_payment_headers iph ON iph.id = ipd.invoice_payment_header_id
19
LEFT JOIN public.users u ON u.id = ipd.created_by
20
WHERE
21
ipd.invoice_header_id = p_invoice_header_id
22
AND ipd.is_deleted = false
23
ORDER BY
24
ipd.created_on_utc ASC;
25
END;
26
$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 | 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 | upsert_invoice_status_company_config | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.upsert_invoice_status_company_config(p_company_id uuid, p_status_id integer, p_is_enabled boolean, p_user_id uuid)
2
RETURNS integer
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_id INT;
7
BEGIN
8
-- Try update first (only non-deleted)
9
UPDATE public.invoice_status_company_configs
10
SET
11
is_enabled = p_is_enabled,
12
modified_on_utc = now(),
13
modified_by = p_user_id
14
WHERE company_id = p_company_id
15
AND status_id = p_status_id
16
AND is_deleted = false
17
RETURNING id INTO v_id;
18
19
-- If not found, insert new with is_deleted = false
20
IF NOT FOUND THEN
21
INSERT INTO public.invoice_status_company_configs (
22
company_id,
23
status_id,
24
is_enabled,
25
created_on_utc,
26
created_by,
27
is_deleted
28
)
29
VALUES (
30
p_company_id,
31
p_status_id,
32
p_is_enabled,
33
now(),
34
p_user_id,
35
false
36
)
37
RETURNING id INTO v_id;
38
END IF;
39
40
RETURN v_id;
41
END;
42
$function$
|
|||||
| Function | update_invoice_next_status_main | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.update_invoice_next_status_main(p_company_id uuid, p_invoice_status_id integer, p_invoice_ids uuid[], p_modified_by uuid)
2
RETURNS TABLE(invoice_id uuid, status_name text, error_msg text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_error_message text;
7
v_invoice_ids_draft uuid[];
8
v_invoice_ids_pending uuid[];
9
v_invoice_ids_other uuid[];
10
v_next_temp_id int;
11
BEGIN
12
RAISE NOTICE 'START update_invoice_next_status_main: company_id=%, invoice_status_id=%, modified_by=%', p_company_id, p_invoice_status_id, p_modified_by;
13
RAISE NOTICE 'Incoming Invoice IDs: %', p_invoice_ids;
14
15
-- Classify invoices by current status
16
SELECT array_agg(id) INTO v_invoice_ids_draft
17
FROM public.draft_invoice_headers dih
18
WHERE dih.id = ANY(p_invoice_ids) AND dih.invoice_status_id = 1;
19
20
SELECT array_agg(id) INTO v_invoice_ids_pending
21
FROM public.draft_invoice_headers dih
22
WHERE dih.id = ANY(p_invoice_ids) AND dih.invoice_status_id = 2;
23
24
SELECT array_agg(id) INTO v_invoice_ids_other
25
FROM public.invoice_headers ih
26
WHERE ih.id = ANY(p_invoice_ids) AND ih.invoice_status_id >= 3;
27
28
RAISE NOTICE 'Draft invoices: %', COALESCE(v_invoice_ids_draft, '{}');
29
RAISE NOTICE 'Pending Approval invoices: %', COALESCE(v_invoice_ids_pending, '{}');
30
RAISE NOTICE 'Other (Approved or higher) invoices: %', COALESCE(v_invoice_ids_other, '{}');
31
32
BEGIN
33
-- Direct update draft invoices to Pending Approval
34
IF array_length(v_invoice_ids_draft, 1) > 0 THEN
35
RAISE NOTICE 'Directly updating Draft invoices to Pending Approval';
36
37
UPDATE public.draft_invoice_headers
38
SET invoice_status_id = 2,
39
modified_by = p_modified_by,
40
modified_on_utc = now()
41
WHERE id = ANY(v_invoice_ids_draft);
42
43
-- Insert approval logs
44
INSERT INTO public.invoice_approval_logs(
45
invoice_id, status_id, approved_by, approved_on, "comment",
46
created_on_utc, created_by, approval_level
47
)
48
SELECT
49
dh.id, 2, p_modified_by, now(),
50
'Direct update from Draft to Pending Approval',
51
now(), p_modified_by, 0
52
FROM public.draft_invoice_headers dh
53
WHERE dh.id = ANY(v_invoice_ids_draft);
54
55
-- Generate next id for temp_invoice_next_status
56
SELECT COALESCE(MAX(id), 0) INTO v_next_temp_id FROM temp_invoice_next_status;
57
58
-- Insert into temp_invoice_next_status
59
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
60
SELECT
61
row_number() OVER () + v_next_temp_id AS id,
62
dh.id,
63
'Pending Approval',
64
NULL
65
FROM public.draft_invoice_headers dh
66
WHERE dh.id = ANY(v_invoice_ids_draft);
67
68
END IF;
69
70
-- Call update_draft_invoice_next_status if Pending Approval invoices found and p_invoice_status_id = 2
71
IF array_length(v_invoice_ids_pending, 1) > 0 AND p_invoice_status_id = 2 THEN
72
RAISE NOTICE 'Calling update_draft_invoice_next_status for Pending Approval invoices';
73
CALL public.update_draft_invoice_next_status(p_company_id, p_invoice_status_id, v_invoice_ids_pending, p_modified_by);
74
END IF;
75
76
-- Call update_invoice_next_status_for_invoice_header for Approved or higher invoices
77
IF array_length(v_invoice_ids_other, 1) > 0 THEN
78
RAISE NOTICE 'Calling update_invoice_next_status_for_invoice_header for Approved or higher invoices';
79
CALL public.update_invoice_next_status_for_invoice_header(p_company_id, p_invoice_status_id, v_invoice_ids_other, p_modified_by);
80
END IF;
81
82
EXCEPTION WHEN OTHERS THEN
83
v_error_message := SQLERRM;
84
RAISE NOTICE 'Exception in child procedures: %', v_error_message;
85
END;
86
87
-- Return rows from temp_invoice_next_status
88
RAISE NOTICE 'Preparing to return rows from temp_invoice_next_status';
89
RETURN QUERY
90
SELECT tinv.invoice_id, tinv.status AS status_name, tinv.error AS error_msg
91
FROM temp_invoice_next_status tinv;
92
93
RAISE NOTICE 'END update_invoice_next_status_main';
94
END;
95
$function$
|
|||||
| Function | update_invoice_next_status_test | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.update_invoice_next_status_test(p_company_id uuid, p_invoice_status_id integer, p_invoice_ids uuid[], p_modified_by uuid)
2
RETURNS TABLE(invoice_id uuid, status text, error text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_draft_invoice RECORD;
7
v_next_status INTEGER;
8
v_account_id UUID;
9
v_user_level INTEGER;
10
v_required_levels INTEGER;
11
v_current_level INTEGER;
12
v_is_account_level BOOLEAN := false;
13
14
APPROVED_STATUS CONSTANT INTEGER := 3;
15
SENDING_FOR_APPROVAL_STATUS CONSTANT INTEGER := 2;
16
BEGIN
17
CREATE TEMP TABLE temp_approval_results (
18
invoice_id uuid,
19
status text,
20
error text
21
) ON COMMIT DROP;
22
23
FOR v_draft_invoice IN
24
SELECT id, invoice_status_id, credit_account_id, current_approval_level
25
FROM public.draft_invoice_headers
26
WHERE id = ANY(p_invoice_ids)
27
LOOP
28
BEGIN
29
RAISE NOTICE 'Processing invoice ID: %', v_draft_invoice.id;
30
31
v_account_id := v_draft_invoice.credit_account_id;
32
v_next_status := p_invoice_status_id;
33
v_current_level := COALESCE(v_draft_invoice.current_approval_level, 1);
34
35
IF p_invoice_status_id = SENDING_FOR_APPROVAL_STATUS THEN
36
RAISE NOTICE 'Sending for approval...';
37
WITH updated AS (
38
UPDATE public.draft_invoice_headers
39
SET invoice_status_id = v_next_status,
40
modified_by = p_modified_by,
41
modified_on_utc = now(),
42
current_approval_level = 1
43
WHERE id = v_draft_invoice.id
44
RETURNING id
45
)
46
INSERT INTO temp_approval_results
47
SELECT
48
u.id,
49
'pending',
50
NULL
51
FROM updated u;
52
CONTINUE;
53
END IF;
54
55
-- Check account-level approval
56
SELECT approval_level INTO v_user_level
57
FROM public.invoice_approval_users_account
58
WHERE company_id = p_company_id
59
AND account_id = v_account_id
60
AND status_id = v_next_status
61
AND user_id = p_modified_by
62
AND is_deleted = false
63
LIMIT 1;
64
65
IF FOUND THEN
66
v_is_account_level := true;
67
ELSE
68
-- Check company-level approval
69
SELECT approval_level INTO v_user_level
70
FROM public.invoice_approval_user_company
71
WHERE company_id = p_company_id
72
AND status_id = v_next_status
73
AND user_id = p_modified_by
74
AND is_deleted = false
75
LIMIT 1;
76
77
IF NOT FOUND THEN
78
RAISE NOTICE 'User % is not authorized for any approval level.', p_modified_by;
79
INSERT INTO temp_approval_results VALUES (
80
v_draft_invoice.id, 'rejected', 'User not authorized for approval'
81
);
82
CONTINUE;
83
END IF;
84
END IF;
85
86
-- Get required approval levels
87
SELECT approval_level_required INTO v_required_levels
88
FROM public.invoice_account_approval_levels
89
WHERE company_id = p_company_id
90
AND account_id = v_account_id
91
AND status_id = v_next_status
92
LIMIT 1;
93
94
v_required_levels := COALESCE(v_required_levels, 1);
95
96
RAISE NOTICE 'User level: %, Required: %, Current: %', v_user_level, v_required_levels, v_current_level;
97
98
IF v_user_level <> v_current_level THEN
99
INSERT INTO temp_approval_results VALUES (
100
v_draft_invoice.id, 'rejected', FORMAT('Approval level mismatch. Expected: %, You have: %', v_current_level, v_user_level)
101
);
102
CONTINUE;
103
END IF;
104
105
-- If not final level, just increment approval level
106
IF v_current_level < v_required_levels THEN
107
RAISE NOTICE 'Incrementing approval level...';
108
UPDATE draft_invoice_headers
109
SET current_approval_level = v_current_level + 1,
110
modified_by = p_modified_by,
111
modified_on_utc = now()
112
WHERE id = v_draft_invoice.id;
113
114
INSERT INTO temp_approval_results VALUES (
115
v_draft_invoice.id, 'pending', FORMAT('Moved to level %', v_current_level + 1)
116
);
117
ELSE
118
-- Final approval and status change
119
RAISE NOTICE 'Final approval and updating status...';
120
UPDATE draft_invoice_headers
121
SET invoice_status_id = v_next_status,
122
current_approval_level = NULL,
123
modified_by = p_modified_by,
124
modified_on_utc = now()
125
WHERE id = v_draft_invoice.id;
126
127
CALL public.log_invoice_approval(
128
v_draft_invoice.id,
129
v_next_status,
130
p_modified_by
131
);
132
133
IF v_next_status = APPROVED_STATUS THEN
134
CALL public.transfer_draft_to_invoice(
135
v_draft_invoice.id,
136
p_modified_by
137
);
138
END IF;
139
140
INSERT INTO temp_approval_results VALUES (
141
v_draft_invoice.id, 'approved', NULL
142
);
143
END IF;
144
145
EXCEPTION WHEN OTHERS THEN
146
RAISE NOTICE 'Error processing invoice ID %: %', v_draft_invoice.id, SQLERRM;
147
INSERT INTO temp_approval_results VALUES (
148
v_draft_invoice.id, 'error', SQLERRM
149
);
150
END;
151
END LOOP;
152
153
RETURN QUERY SELECT * FROM temp_approval_results;
154
END;
155
$function$
|
|||||
| Function | audit_queue_invoice_headers_trigger | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.audit_queue_invoice_headers_trigger()
2
RETURNS trigger
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_changed_by uuid;
7
BEGIN
8
v_changed_by := current_setting('app.current_user_id', true)::uuid;
9
10
IF TG_OP = 'UPDATE' THEN
11
INSERT INTO audit_queue(table_name, row_id, old_data, new_data, changed_by)
12
VALUES ('invoice_headers', NEW.id, to_jsonb(OLD), to_jsonb(NEW), v_changed_by);
13
14
ELSIF TG_OP = 'INSERT' THEN
15
INSERT INTO audit_queue(table_name, row_id, old_data, new_data, changed_by)
16
VALUES ('invoice_headers', NEW.id, NULL, to_jsonb(NEW), v_changed_by);
17
18
ELSIF TG_OP = 'DELETE' THEN
19
INSERT INTO audit_queue(table_name, row_id, old_data, new_data, changed_by)
20
VALUES ('invoice_headers', OLD.id, to_jsonb(OLD), NULL, v_changed_by);
21
22
END IF;
23
24
PERFORM pg_notify('audit_event', currval('audit_queue_audit_queue_id_seq')::text);
25
26
RETURN NEW;
27
END;
28
$function$
|
|||||
| Function | edit_draft_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.edit_draft_invoice(p_invoice_id uuid, p_company_id uuid, p_customer_id uuid, p_currency_id integer, p_invoice_date timestamp without time zone, p_payment_term integer, p_invoice_status_id integer, p_due_date timestamp without time zone, p_total_amount numeric, p_taxable_amount numeric, p_discount numeric, p_fees numeric, p_sgst_amount numeric, p_cgst_amount numeric, p_igst_amount numeric, p_note character varying, p_round_off numeric, p_source_warehouse_id uuid, p_destination_warehouse_id uuid, p_modified_by uuid, p_invoice_details jsonb)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
-- Update the draft invoice header
7
UPDATE draft_invoice_header
8
SET
9
company_id = p_company_id,
10
customer_id = p_customer_id,
11
currency_id = p_currency_id,
12
invoice_date = p_invoice_date,
13
payment_term = p_payment_term,
14
invoice_status_id = p_invoice_status_id,
15
due_date = p_due_date,
16
total_amount = p_total_amount,
17
taxable_amount = p_taxable_amount,
18
discount = p_discount,
19
fees = p_fees,
20
sgst_amount = p_sgst_amount,
21
cgst_amount = p_cgst_amount,
22
igst_amount = p_igst_amount,
23
note = p_note,
24
round_off = p_round_off,
25
source_warehouse_id = p_source_warehouse_id,
26
destination_warehouse_id = p_destination_warehouse_id,
27
modified_by = p_modified_by,
28
modified_on = NOW()
29
WHERE id = p_invoice_id;
30
31
-- Delete existing invoice details for the draft invoice
32
DELETE FROM draft_invoice_detail
33
WHERE invoice_id = p_invoice_id;
34
35
-- Insert the new invoice details
36
INSERT INTO draft_invoice_detail (
37
invoice_id,
38
product_id,
39
quantity,
40
price,
41
taxable_amount,
42
tax_amount,
43
total_amount
44
)
45
SELECT
46
p_invoice_id,
47
(jsonb_detail->>'product_id')::uuid,
48
(jsonb_detail->>'quantity')::numeric,
49
(jsonb_detail->>'price')::numeric,
50
(jsonb_detail->>'taxable_amount')::numeric,
51
(jsonb_detail->>'tax_amount')::numeric,
52
(jsonb_detail->>'total_amount')::numeric
53
FROM jsonb_array_elements(p_invoice_details) as jsonb_detail;
54
55
-- You can add more logic as necessary
56
END;
57
$function$
|
|||||
| Function | get_customer_details | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_customer_details(p_customer_id uuid)
2
RETURNS TABLE(id uuid, name character varying, email text, mobile_number text, phone_number text, gstin text, short_name text, pan text, tan text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
c.id,
9
c.name,
10
ct.email,
11
ct.mobile_number,
12
ct.phone_number,
13
c.gstin,
14
c.short_name,
15
c.pan,
16
c.tan
17
FROM customers c
18
LEFT JOIN customer_contacts cc ON cc.customer_id = c.id
19
LEFT JOIN contacts ct ON cc.contact_id = ct.id
20
WHERE c.id = p_customer_id
21
ORDER BY ct.is_primary DESC
22
LIMIT 1;
23
END;
24
$function$
|
|||||
| Function | check_penalty_date | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.check_penalty_date(due_date date, penalty_check_date date)
2
RETURNS boolean
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
first_penalty_date DATE;
7
adjusted_penalty_date DATE;
8
last_day_of_month DATE;
9
BEGIN
10
-- Calculate the first penalty date (1 day after the due date)
11
first_penalty_date := due_date + INTERVAL '1 day';
12
13
-- If the penalty_check_date is before the first penalty date, return FALSE
14
IF penalty_check_date < first_penalty_date THEN
15
RETURN FALSE;
16
END IF;
17
18
-- Calculate the last day of the month for the penalty_check_date
19
last_day_of_month := DATE_TRUNC('month', penalty_check_date) + INTERVAL '1 month' - INTERVAL '1 day';
20
21
-- Calculate the adjusted penalty date for the month of the penalty_check_date
22
IF EXTRACT(DAY FROM first_penalty_date) > EXTRACT(DAY FROM last_day_of_month) THEN
23
adjusted_penalty_date := last_day_of_month;
24
ELSE
25
adjusted_penalty_date := DATE_TRUNC('month', penalty_check_date) + (EXTRACT(DAY FROM first_penalty_date) - 1) * INTERVAL '1 day';
26
END IF;
27
28
-- Return TRUE if the penalty_check_date matches the adjusted_penalty_date
29
RETURN penalty_check_date = adjusted_penalty_date;
30
END;
31
$function$
|
|||||
| Function | get_group_invoice_by_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_group_invoice_by_id(p_group_invoice_id uuid)
2
RETURNS TABLE(id uuid, execution_date timestamp without time zone, group_invoice_number text, invoice_description text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
gih.id,
9
gih.execution_date,
10
gih.group_invoice_number,
11
git.invoice_description
12
FROM
13
public.group_invoice_headers gih
14
INNER JOIN
15
public.group_invoice_templates git
16
ON gih.group_invoice_template_id = git.id
17
WHERE
18
gih.id = p_group_invoice_id; -- Use the function parameter here
19
END;
20
$function$
|
|||||
| Function | get_all_invoice_company_approvals | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_invoice_company_approvals(p_company_id uuid)
2
RETURNS TABLE(id integer, status_id integer, user_id uuid, approval_level integer, required_approval_levels integer)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
MIN(ia.id) AS id,
9
ia.status_id,
10
ia.user_id,
11
ia.approval_level,
12
iw.approval_level AS required_approval_levels
13
FROM invoice_approval_user_company ia
14
JOIN invoice_workflow iw
15
ON ia.company_id = iw.company_id
16
WHERE ia.company_id = p_company_id
17
AND ia.is_deleted = false
18
AND iw.is_deleted = false
19
AND iw.is_enabled = TRUE
20
AND iw.status = 2
21
GROUP BY ia.status_id, ia.user_id, ia.approval_level,iw.approval_level
22
ORDER BY ia.status_id, ia.user_id;
23
END;
24
$function$
|
|||||
| Function | soft_delete_invoice_if_no_details | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.soft_delete_invoice_if_no_details(p_invoice_id uuid, p_modified_by uuid)
2
RETURNS text
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
-- Final invoice case
7
IF EXISTS (SELECT 1 FROM invoice_headers WHERE id = p_invoice_id) THEN
8
IF NOT EXISTS (SELECT 1 FROM invoice_details WHERE invoice_header_id = p_invoice_id) THEN
9
UPDATE invoice_headers
10
SET
11
is_deleted = TRUE,
12
deleted_on_utc = NOW(),
13
modified_by = p_modified_by
14
WHERE id = p_invoice_id;
15
16
RETURN 'Soft-deleted from invoice_headers';
17
ELSE
18
RETURN 'Cannot delete: invoice_details exist';
19
END IF;
20
END IF;
21
22
-- Draft invoice case
23
IF EXISTS (SELECT 1 FROM draft_invoice_headers WHERE id = p_invoice_id) THEN
24
IF NOT EXISTS (SELECT 1 FROM draft_invoice_details WHERE draft_invoice_header_id = p_invoice_id) THEN
25
UPDATE draft_invoice_headers
26
SET
27
is_deleted = TRUE,
28
deleted_on_utc = NOW(),
29
modified_by = p_modified_by
30
WHERE id = p_invoice_id;
31
32
RETURN 'Soft-deleted from draft_invoice_headers';
33
ELSE
34
RETURN 'Cannot delete: draft_invoice_details exist';
35
END IF;
36
END IF;
37
38
RETURN 'Invoice not found';
39
END;
40
$function$
|
|||||
| Function | upsert_invoice_workflow_and_config | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.upsert_invoice_workflow_and_config(p_company_id uuid, p_config jsonb, p_user_id uuid)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
item JSONB;
7
v_status INT;
8
v_next_status INT;
9
v_previous_status INT;
10
v_is_initial BOOL;
11
v_is_enabled BOOL;
12
v_approval_level INT;
13
BEGIN
14
FOR item IN SELECT * FROM jsonb_array_elements(p_config)
15
LOOP
16
v_status := (item->>'Status')::INT;
17
v_next_status := (item->>'NextStatus')::INT;
18
v_previous_status := (item->>'PreviousStatus')::INT;
19
v_is_initial := (item->>'IsInitial')::BOOL;
20
v_is_enabled := (item->>'IsEnabled')::BOOL;
21
v_approval_level := (item->>'ApprovalLevel')::INT;
22
23
-- UPSERT invoice_workflow
24
IF EXISTS (
25
SELECT 1 FROM invoice_workflow
26
WHERE company_id = p_company_id
27
AND status = v_status
28
AND next_status = v_next_status
29
AND previous_status = v_previous_status
30
AND is_deleted = false
31
) THEN
32
UPDATE invoice_workflow
33
SET
34
is_initial = v_is_initial,
35
is_enabled = v_is_enabled,
36
approval_level = v_approval_level,
37
modified_on_utc = now(),
38
modified_by = p_user_id
39
WHERE company_id = p_company_id
40
AND status = v_status
41
AND next_status = v_next_status
42
AND previous_status = v_previous_status
43
AND is_deleted = false;
44
ELSE
45
INSERT INTO invoice_workflow (
46
company_id, status, next_status, previous_status,
47
is_initial, created_on_utc, is_deleted, created_by,
48
is_enabled, approval_level
49
)
50
VALUES (
51
p_company_id, v_status, v_next_status, v_previous_status,
52
v_is_initial, now(), false, p_user_id,
53
v_is_enabled, v_approval_level
54
);
55
END IF;
56
57
-- UPSERT invoice_status_company_configs
58
INSERT INTO invoice_status_company_configs (
59
company_id, status_id, is_enabled,
60
created_on_utc, created_by, is_deleted
61
)
62
VALUES (
63
p_company_id, v_status, v_is_enabled,
64
now(), p_user_id, false
65
)
66
ON CONFLICT (company_id, status_id)
67
DO UPDATE SET
68
is_enabled = EXCLUDED.is_enabled,
69
modified_on_utc = now(),
70
modified_by = p_user_id;
71
END LOOP;
72
END;
73
$function$
|
|||||
| Function | list_scheduled_group_invoice_template_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.list_scheduled_group_invoice_template_ids(p_run_date timestamp without time zone)
2
RETURNS TABLE(group_invoice_template_id uuid, company_id uuid, organization_id uuid)
3
LANGUAGE sql
4
STABLE
5
AS $function$
6
WITH params AS (
7
SELECT
8
(p_run_date)::date AS run_date,
9
EXTRACT(ISODOW FROM p_run_date)::int AS run_isodow,
10
EXTRACT(DAY FROM p_run_date)::int AS run_day,
11
EXTRACT(MONTH FROM p_run_date)::int AS run_month,
12
EXTRACT(DAY FROM (date_trunc('month', (p_run_date)::date)
13
+ interval '1 month - 1 day'))::int AS days_in_month
14
),
15
fy AS (
16
SELECT *,
17
CASE WHEN run_month >= 4 THEN run_month - 3 ELSE run_month + 9 END AS fy_month_index
18
FROM params
19
),
20
base AS (
21
SELECT
22
-- schedule fields (explicit; no bs.*)
23
bs.billing_cycle,
24
bs.day_of_week,
25
bs.day_of_month,
26
bs.bi_month,
27
bs.quarters_of_month,
28
bs.half_year,
29
bs.month_of_year,
30
bs.last_executed_at,
31
32
-- template & org linkage
33
git.id AS group_invoice_template_id,
34
c.id AS company_id,
35
o.id AS organization_id,
36
37
-- template window
38
git.starts_from,
39
git.end_date,
40
41
-- precomputed calendar
42
f.run_date,
43
f.run_isodow,
44
f.run_day,
45
f.days_in_month,
46
f.fy_month_index
47
FROM public.batch_schedules bs
48
JOIN public.group_invoice_templates git
49
ON git.id = bs.group_invoice_template_id
50
AND git.is_deleted = FALSE
51
AND git.active_status = TRUE
52
JOIN public.companies c
53
ON c.id = git.company_id
54
AND c.is_deleted = FALSE
55
JOIN public.organizations o
56
ON o.id = c.organization_id
57
AND o.is_deleted = FALSE
58
CROSS JOIN fy f
59
WHERE (bs.last_executed_at IS NULL OR bs.last_executed_at::date <> f.run_date)
60
AND (git.starts_from IS NULL OR f.run_date >= git.starts_from::date)
61
AND (git.end_date IS NULL OR f.run_date <= git.end_date::date)
62
),
63
calc AS (
64
SELECT
65
b.group_invoice_template_id,
66
b.company_id,
67
b.organization_id,
68
CASE b.billing_cycle
69
WHEN 'Daily' THEN TRUE
70
WHEN 'Weekly' THEN COALESCE(b.day_of_week, b.run_isodow) = b.run_isodow
71
WHEN 'Monthly' THEN b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
72
WHEN 'Bi-Monthly' THEN
73
COALESCE(b.bi_month, 1) IN (1,2)
74
AND (
75
(b.bi_month = 1 AND (b.fy_month_index % 2) = 1) OR
76
(b.bi_month = 2 AND (b.fy_month_index % 2) = 0)
77
)
78
AND b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
79
WHEN 'Quarterly' THEN
80
COALESCE(b.quarters_of_month, 1) BETWEEN 1 AND 4
81
AND b.fy_month_index = (CASE b.quarters_of_month
82
WHEN 1 THEN 1
83
WHEN 2 THEN 4
84
WHEN 3 THEN 7
85
ELSE 10 END)
86
AND b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
87
WHEN 'Half-yearly' THEN
88
COALESCE(b.half_year, 1) IN (1,2)
89
AND b.fy_month_index = (CASE b.half_year WHEN 1 THEN 1 ELSE 7 END)
90
AND b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
91
WHEN 'Yearly' THEN
92
COALESCE(b.month_of_year, b.fy_month_index) = b.fy_month_index
93
AND b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
94
ELSE FALSE
95
END AS is_due
96
FROM base b
97
)
98
SELECT
99
group_invoice_template_id,
100
company_id,
101
organization_id
102
FROM calc
103
WHERE is_due = TRUE
104
ORDER BY organization_id, company_id, group_invoice_template_id;
105
$function$
|
|||||
| Function | get_grouped_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_grouped_invoice(p_company_id uuid)
2
RETURNS TABLE(id uuid, template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
executions.id,
9
executions.template_id,
10
executions.execution_date,
11
executions.error_message,
12
executions.success_count,
13
executions.total_count,
14
templates.invoice_description
15
FROM
16
public.apartment_invoice_template_executions executions
17
INNER JOIN
18
public.apartment_invoice_templates templates
19
ON
20
executions.template_id = templates.id
21
WHERE
22
executions.company_id = p_company_id
23
24
ORDER BY
25
executions.execution_date DESC;
26
END;
27
$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_grouped_invoice_header | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_header(p_company_id uuid)
2
RETURNS TABLE(id uuid, group_invoice_number text, grouped_invoice_template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text, group_invoice_batch_id text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
gih.id,
9
gih.group_invoice_number,
10
gih.group_invoice_template_id AS grouped_invoice_template_id,
11
gih.execution_date,
12
gih.error_message,
13
gih.success_count,
14
gih.total_count,
15
templates.invoice_description,
16
gih.group_invoice_batch_id
17
FROM
18
public.group_invoice_headers gih
19
INNER JOIN
20
public.group_invoice_templates templates
21
ON
22
gih.group_invoice_template_id = templates.id
23
WHERE
24
gih.company_id = p_company_id
25
ORDER BY
26
gih.execution_date DESC;
27
END;
28
$function$
|
|||||
| Function | get_invoice_header_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_header_ids(p_company_id uuid, p_fin_year integer)
2
RETURNS TABLE(company_id uuid, fin_year integer, draft_invoice_prefix text, draft_invoice_length integer, invoice_prefix text, invoice_length integer, payment_prefix text, payment_length integer)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
COALESCE(draft.company_id, invoice.company_id, payment.company_id) AS company_id,
9
COALESCE(draft.fin_year, invoice.fin_year, payment.fin_year) AS fin_year,
10
draft.invoice_prefix AS draft_invoice_prefix,
11
draft.invoice_length AS draft_invoice_length,
12
invoice.invoice_prefix AS invoice_prefix,
13
invoice.invoice_length AS invoice_length,
14
payment.payment_prefix AS payment_prefix,
15
payment.payment_length AS payment_length
16
FROM
17
(SELECT d.company_id, d.fin_year, d.invoice_prefix, d.invoice_length FROM draft_invoice_header_ids as d) AS draft
18
FULL OUTER JOIN (SELECT i.company_id, i.fin_year, i.invoice_prefix, i.invoice_length FROM invoice_header_ids as i) AS invoice
19
ON draft.company_id = invoice.company_id AND draft.fin_year = invoice.fin_year
20
FULL OUTER JOIN (SELECT p.company_id, p.fin_year, p.payment_prefix, p.payment_length FROM invoice_payment_header_ids as p) AS payment
21
ON draft.company_id = payment.company_id AND draft.fin_year = payment.fin_year
22
OR invoice.company_id = payment.company_id AND invoice.fin_year = payment.fin_year
23
WHERE
24
COALESCE(draft.company_id, invoice.company_id, payment.company_id) = p_company_id
25
AND COALESCE(draft.fin_year, invoice.fin_year, payment.fin_year) = p_fin_year;
26
END;
27
$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, debit numeric, credit numeric, closing_balance numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
finYearStartDate DATE;
7
finYearLastDate DATE;
8
v_start_date DATE;
9
v_end_date DATE;
10
BEGIN
11
-- Compute Financial Year Start & End Dates
12
finYearStartDate := MAKE_DATE(p_fin_year_id, 4, 1); -- April 1st of the financial year
13
finYearLastDate := MAKE_DATE(p_fin_year_id + 1, 3, 31); -- March 31st of next year
14
15
-- Override with provided start_date and end_date if available
16
v_start_date := COALESCE(p_start_date, finYearStartDate);
17
v_end_date := COALESCE(p_end_date, finYearLastDate);
18
19
RETURN QUERY
20
WITH trial_balance AS (
21
SELECT
22
je.account_id,
23
je.account_name AS ledger,
24
je.account_category AS general_ledger,
25
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS total_debits,
26
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS total_credits
27
FROM public.journal_entries je
28
INNER JOIN public.transaction_headers th
29
ON je.transaction_id = th.id
30
WHERE th.company_id = p_company_id
31
AND th.transaction_date BETWEEN v_start_date AND v_end_date
32
GROUP BY je.account_id, je.account_name, je.account_category
33
),
34
opening_balances AS (
35
SELECT
36
je.account_id,
37
SUM(CASE
38
WHEN je.entry_type = 'D' THEN je.amount
39
ELSE -je.amount
40
END) AS opening_balance
41
FROM public.journal_entries je
42
INNER JOIN public.transaction_headers th
43
ON je.transaction_id = th.id
44
WHERE th.company_id = p_company_id
45
AND th.transaction_date < v_start_date
46
GROUP BY je.account_id
47
)
48
SELECT
49
ROW_NUMBER() OVER()::INTEGER AS sl_no,
50
tb.ledger,
51
tb.general_ledger,
52
COALESCE(ob.opening_balance, 0) AS opening_balance,
53
COALESCE(tb.total_debits, 0) AS debit,
54
COALESCE(tb.total_credits, 0) AS credit,
55
(COALESCE(ob.opening_balance, 0) +
56
COALESCE(tb.total_debits, 0) -
57
COALESCE(tb.total_credits, 0)) AS closing_balance
58
FROM trial_balance tb
59
LEFT JOIN opening_balances ob
60
ON tb.account_id = ob.account_id
61
ORDER BY tb.ledger;
62
END;
63
$function$
|
|||||
| Function | get_warehouse_details | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_warehouse_details(p_warehouse_id uuid)
2
RETURNS TABLE(id uuid, customer_id uuid, name text, address_id uuid, country_name text, country_id uuid, state_name text, state_id uuid, city_name text, city_id uuid, address_line1 text, address_line2 text, zip_code text, gstin text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
wh.id,
9
wh.customer_id,
10
wh.name,
11
wh.address_id,
12
(SELECT cou.name FROM countries cou WHERE cou.id = a.country_id) AS country_name,
13
a.country_id,
14
(SELECT st.name FROM states st WHERE st.id = a.state_id) AS state_name,
15
a.state_id,
16
(SELECT ci.name FROM cities ci WHERE ci.id = a.city_id) AS city_name,
17
a.city_id,
18
a.address_line1,
19
a.address_line2,
20
a.zip_code,
21
wh.gstin
22
FROM warehouses wh
23
JOIN addresses a ON wh.address_id = a.id
24
WHERE wh.id = p_warehouse_id;
25
26
IF NOT FOUND THEN
27
RETURN; -- Return an empty result set if no warehouse is found
28
END IF;
29
END;
30
$function$
|
|||||
| Function | close_resolved_cycles | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.close_resolved_cycles(p_organization_id uuid, p_company_ids uuid[])
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
UPDATE delinquency_cycles dc
7
SET
8
ended_on_utc = now(),
9
status_id = 2, -- CLOSED
10
modified_on_utc = now(),
11
modified_by = get_system_user_id()
12
WHERE dc.organization_id = p_organization_id
13
AND dc.ended_on_utc IS NULL
14
AND dc.is_deleted = false
15
AND NOT EXISTS (
16
SELECT 1
17
FROM invoice_headers ih
18
WHERE ih.company_id = ANY(p_company_ids)
19
AND ih.customer_id = dc.customer_id
20
AND ih.is_deleted = false
21
AND (ih.total_amount - ih.settled_amount) > 0
22
);
23
END;
24
$function$
|
|||||
| Function | update_invoice_payment_status | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.update_invoice_payment_status(p_company_id uuid, p_invoice_ids uuid[], p_payment_status_id integer, p_modified_by uuid)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_invoice_header_id uuid; -- Declare a variable to hold each invoice_header_id
7
v_invoice_payment_header public.invoice_payment_headers%ROWTYPE; -- Declare ROWTYPE for full row
8
BEGIN
9
-- Loop over the provided array of Invoice Header IDs
10
FOREACH v_invoice_header_id IN ARRAY p_invoice_ids
11
LOOP
12
-- Log the current invoice being processed
13
RAISE NOTICE 'Processing InvoiceHeaderId: %, PaymentStatusId: %, CompanyId: %, ModifiedBy: %',
14
v_invoice_header_id, p_payment_status_id, p_company_id, p_modified_by;
15
16
-- Retrieve the corresponding InvoicePaymentDetail for the given invoice_header_id
17
SELECT invoice_payment_header_id INTO v_invoice_payment_header.id
18
FROM public.invoice_payment_details
19
WHERE invoice_header_id = v_invoice_header_id -- Match invoice header ID in invoice_payment_details
20
AND is_deleted = false
21
LIMIT 1;
22
23
-- If payment detail exists, proceed to update payment header
24
IF FOUND THEN
25
RAISE NOTICE 'Invoice Payment Detail found for InvoiceHeaderId: %', v_invoice_header_id;
26
27
-- Retrieve the corresponding InvoicePaymentHeader using the payment header id
28
SELECT * INTO v_invoice_payment_header
29
FROM public.invoice_payment_headers
30
WHERE id = v_invoice_payment_header.id -- Match invoice payment header ID
31
AND is_deleted = false -- Ensure not deleted
32
LIMIT 1;
33
34
-- If the payment header exists, update its PaymentStatusId
35
IF FOUND THEN
36
RAISE NOTICE 'Updating PaymentStatusId for InvoicePaymentHeaderId: %', v_invoice_payment_header.id;
37
38
UPDATE public.invoice_payment_headers
39
SET payment_status_id = p_payment_status_id,
40
modified_by = p_modified_by,
41
modified_on_utc = NOW()
42
WHERE id = v_invoice_payment_header.id;
43
44
RAISE NOTICE 'PaymentStatusId updated for InvoicePaymentHeaderId: %', v_invoice_payment_header.id;
45
ELSE
46
RAISE NOTICE 'InvoicePaymentHeader not found for InvoiceHeaderId: %', v_invoice_header_id;
47
END IF;
48
ELSE
49
RAISE NOTICE 'InvoicePaymentDetail not found for InvoiceHeaderId: %', v_invoice_header_id;
50
END IF;
51
END LOOP;
52
53
-- No need for COMMIT, as the transaction will be committed by the caller
54
RAISE NOTICE 'Updated payment status for % invoices.', array_length(p_invoice_ids, 1);
55
END;
56
$function$
|
|||||
| Function | get_invoices_by_customer_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoices_by_customer_id(p_company_id uuid, p_fin_year_id integer, p_user_id uuid, p_customer_id uuid, p_type_id integer DEFAULT NULL::integer, p_start_date timestamp without time zone DEFAULT NULL::timestamp without time zone, p_end_date timestamp without time zone DEFAULT NULL::timestamp without time zone)
2
RETURNS TABLE(id uuid, invoice_number character varying, type integer, invoice_date date, customer_id uuid, customer_name character varying, due_date date, total_amount numeric, settled_amount numeric, invoice_status_id integer, invoice_status character varying, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, created_by uuid, modified_by uuid, created_by_name character varying, modified_by_name character varying, is_created_by_scheduler boolean, has_next_status_approval boolean, next_status_id integer)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date := COALESCE(p_start_date::date, MAKE_DATE(p_fin_year_id, 4, 1));
7
v_end_date date := COALESCE(p_end_date::date, MAKE_DATE(p_fin_year_id + 1, 3, 31));
8
DRAFT_STATUS CONSTANT integer := 1;
9
BEGIN
10
RETURN QUERY
11
WITH invoice_data AS (
12
-- Posted/normal invoices
13
SELECT
14
ih.id,
15
CAST(ih.invoice_number AS varchar) AS invoice_number,
16
ih.type,
17
ih.invoice_date::date AS invoice_date,
18
ih.customer_id,
19
c.name AS customer_name,
20
ih.due_date::date AS due_date,
21
ih.total_amount,
22
ih.settled_amount,
23
ih.invoice_status_id,
24
s.name AS invoice_status,
25
ih.currency_id,
26
ih.created_on_utc,
27
ih.modified_on_utc,
28
ih.created_by,
29
ih.modified_by,
30
CAST(CONCAT(cu.first_name, ' ', cu.last_name) AS varchar) AS created_by_name,
31
CAST(CONCAT(mu.first_name, ' ', mu.last_name) AS varchar) AS modified_by_name,
32
ih.is_created_by_scheduler,
33
ih.credit_account_id
34
FROM public.invoice_headers ih
35
LEFT JOIN public.customers c ON c.id = ih.customer_id
36
LEFT JOIN public.invoice_statuses s ON s.id = ih.invoice_status_id
37
LEFT JOIN public.users cu ON cu.id = ih.created_by
38
LEFT JOIN public.users mu ON mu.id = ih.modified_by
39
WHERE ih.company_id = p_company_id
40
AND ih.customer_id = p_customer_id
41
AND ih.is_deleted = false
42
AND (c.is_deleted = false OR c.is_deleted IS NULL)
43
AND ih.invoice_date BETWEEN v_start_date AND v_end_date
44
AND (p_type_id IS NULL OR p_type_id = 0 OR ih.type = p_type_id)
45
46
UNION ALL
47
48
-- Draft invoices
49
SELECT
50
dih.id,
51
CAST(dih.invoice_number AS varchar) AS invoice_number,
52
dih.type,
53
dih.invoice_date::date AS invoice_date,
54
dih.customer_id,
55
c.name AS customer_name,
56
dih.due_date::date AS due_date,
57
dih.total_amount,
58
dih.settled_amount,
59
dih.invoice_status_id,
60
s.name AS invoice_status,
61
dih.currency_id,
62
dih.created_on_utc,
63
dih.modified_on_utc,
64
dih.created_by,
65
dih.modified_by,
66
CAST(CONCAT(cu.first_name, ' ', cu.last_name) AS varchar) AS created_by_name,
67
CAST(CONCAT(mu.first_name, ' ', mu.last_name) AS varchar) AS modified_by_name,
68
dih.is_created_by_scheduler,
69
dih.credit_account_id
70
FROM public.draft_invoice_headers dih
71
LEFT JOIN public.customers c ON c.id = dih.customer_id
72
LEFT JOIN public.invoice_statuses s ON s.id = dih.invoice_status_id
73
LEFT JOIN public.users cu ON cu.id = dih.created_by
74
LEFT JOIN public.users mu ON mu.id = dih.modified_by
75
WHERE dih.company_id = p_company_id
76
AND dih.customer_id = p_customer_id
77
AND dih.is_deleted = false
78
AND (c.is_deleted = false OR c.is_deleted IS NULL)
79
AND dih.invoice_date BETWEEN v_start_date AND v_end_date
80
AND (p_type_id IS NULL OR p_type_id = 0 OR dih.type = p_type_id)
81
)
82
SELECT
83
id.id,
84
id.invoice_number,
85
id.type,
86
id.invoice_date,
87
id.customer_id,
88
id.customer_name,
89
id.due_date,
90
id.total_amount,
91
id.settled_amount,
92
id.invoice_status_id,
93
id.invoice_status,
94
id.currency_id,
95
id.created_on_utc,
96
id.modified_on_utc,
97
id.created_by,
98
id.modified_by,
99
id.created_by_name,
100
id.modified_by_name,
101
id.is_created_by_scheduler,
102
103
/* Same approval logic as get_all_invoices_from_span */
104
CASE
105
WHEN id.invoice_status_id = DRAFT_STATUS THEN true
106
WHEN EXISTS (
107
SELECT 1
108
FROM public.invoice_approval_users_account a
109
WHERE a.account_id = id.credit_account_id
110
AND a.status_id = id.invoice_status_id
111
AND a.user_id = p_user_id
112
AND a.is_deleted = false
113
) THEN true
114
WHEN EXISTS (
115
SELECT 1
116
FROM public.invoice_approval_user_company c
117
WHERE c.company_id = p_company_id
118
AND c.status_id = id.invoice_status_id
119
AND c.user_id = p_user_id
120
AND c.is_deleted = false
121
) THEN true
122
ELSE false
123
END AS has_next_status_approval,
124
125
/* Same next-status resolution as get_all_invoices_from_span */
126
COALESCE(
127
(SELECT iw.next_status
128
FROM public.invoice_workflow iw
129
WHERE iw.company_id = p_company_id
130
AND iw.status = id.invoice_status_id
131
AND iw.is_deleted = false
132
LIMIT 1),
133
0
134
) AS next_status_id
135
FROM invoice_data id;
136
END;
137
$function$
|
|||||
| Function | list_scheduled_invoice_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.list_scheduled_invoice_ids(p_schedule_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP)
2
RETURNS TABLE(invoice_header_id uuid, draft_invoice_header_id uuid, company_id uuid, organization_id uuid)
3
LANGUAGE sql
4
AS $function$
5
WITH base AS (
6
SELECT
7
s.id AS schedule_id,
8
s.invoice_header_id AS invoice_header_id,
9
s.draft_invoice_header_id AS draft_invoice_header_id,
10
s.company_id AS company_id,
11
c.organization_id AS organization_id,
12
d.frequency_cycle,
13
d.day_of_week,
14
d.day_of_month,
15
d.month_of_year,
16
s.starts_from,
17
s.end_date
18
FROM public.recurring_sales_schedules s
19
JOIN public.recurrence_schedule_details d
20
ON d.schedule_id = s.id
21
AND d.is_deleted = false
22
JOIN public.companies c
23
ON c.id = s.company_id
24
WHERE s.is_deleted = false
25
AND s.schedule_status = 'active'
26
AND s.starts_from <= (p_schedule_date::date)
27
AND (s.end_date IS NULL OR s.end_date >= (p_schedule_date::date))
28
),
29
aligned AS (
30
SELECT
31
b.schedule_id,
32
b.invoice_header_id,
33
b.draft_invoice_header_id,
34
b.company_id,
35
b.organization_id
36
FROM base b
37
WHERE CASE lower(b.frequency_cycle)
38
WHEN 'daily' THEN TRUE
39
WHEN 'weekly' THEN b.day_of_week IS NOT NULL
40
AND b.day_of_week = EXTRACT(ISODOW FROM p_schedule_date)::int
41
WHEN 'monthly' THEN b.day_of_month IS NOT NULL
42
AND b.day_of_month = EXTRACT(DAY FROM p_schedule_date)::int
43
WHEN 'yearly' THEN b.month_of_year IS NOT NULL AND b.day_of_month IS NOT NULL
44
AND b.month_of_year = EXTRACT(MONTH FROM p_schedule_date)::int
45
AND b.day_of_month = EXTRACT(DAY FROM p_schedule_date)::int
46
ELSE FALSE
47
END
48
)
49
SELECT DISTINCT
50
a.invoice_header_id,
51
a.draft_invoice_header_id,
52
a.company_id,
53
a.organization_id
54
FROM aligned a
55
WHERE NOT EXISTS (
56
SELECT 1
57
FROM public.schedule_execution_logs l
58
WHERE l.schedule_id = a.schedule_id
59
AND l.execution_date = (p_schedule_date::date)
60
AND l.is_deleted = false
61
);
62
$function$
|
|||||
| Function | get_invoice_by_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_by_id(p_invoice_header_id uuid)
2
RETURNS TABLE(id uuid, invoice_number text, credit_account_id uuid, invoice_voucher text, invoice_date timestamp with time zone, payment_term integer, customer_id uuid, customer_name character varying, customer_email text, customer_mobile_number text, customer_phone_number text, customer_gstin text, customer_short_name text, customer_pan text, customer_tan text, due_date timestamp with time zone, total_amount numeric, taxable_amount numeric, fees numeric, cgst_amount numeric, sgst_amount numeric, igst_amount numeric, settled_amount numeric, currency_id integer, invoice_status_id integer, invoice_status text, discount numeric, note text, round_off numeric, source_warehouse_id uuid, destination_warehouse_id uuid, destination_customer_id uuid, destination_warehouse_name text, destination_address_id uuid, destination_country_name text, destination_country_id uuid, destination_state_name text, destination_state_id uuid, destination_city_name text, destination_city_id uuid, destination_address_line1 text, destination_address_line2 text, destination_zip_code text, destination_gstin text, invoice_lines jsonb, type integer, current_approval_level integer, is_created_by_scheduler boolean, created_on_utc timestamp with time zone, modified_on_utc timestamp with time zone, created_by uuid, modified_by uuid)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_invoice_status_id integer;
7
BEGIN
8
-- Get the invoice status using the get_invoice_status function
9
v_invoice_status_id := public.get_invoice_status(p_invoice_header_id);
10
11
IF v_invoice_status_id >= 3 THEN
12
RETURN QUERY
13
SELECT
14
ih.id,
15
ih.invoice_number,
16
ih.credit_account_id,
17
ih.invoice_voucher,
18
ih.invoice_date::timestamp with time zone,
19
ih.payment_term,
20
ih.customer_id,
21
c.name AS customer_name,
22
c.email AS customer_email,
23
c.mobile_number AS customer_mobile_number,
24
c.phone_number AS customer_phone_number,
25
c.gstin AS customer_gstin,
26
c.short_name AS customer_short_name,
27
c.pan AS customer_pan,
28
c.tan AS customer_tan,
29
ih.due_date::timestamp with time zone,
30
ih.total_amount,
31
ih.taxable_amount,
32
ih.fees,
33
ih.cgst_amount,
34
ih.sgst_amount,
35
ih.igst_amount,
36
ih.settled_amount,
37
ih.currency_id,
38
ih.invoice_status_id,
39
invst.name :: text,
40
ih.discount,
41
ih.note,
42
ih.round_off,
43
ih.source_warehouse_id,
44
wd.id AS destination_warehouse_id,
45
wd.customer_id AS destination_customer_id,
46
wd.name AS destination_warehouse_name,
47
wd.address_id AS destination_address_id,
48
wd.country_name AS destination_country_name,
49
wd.country_id AS destination_country_id,
50
wd.state_name AS destination_state_name,
51
wd.state_id AS destination_state_id,
52
wd.city_name AS destination_city_name,
53
wd.city_id AS destination_city_id,
54
wd.address_line1 AS destination_address_line1,
55
wd.address_line2 AS destination_address_line2,
56
wd.zip_code AS destination_zip_code,
57
wd.gstin AS destination_gstin,
58
jsonb_agg(jsonb_build_object(
59
'invoice_line_id', il.id,
60
'product_id', il.product_id,
61
'price', il.price,
62
'quantity', il.quantity,
63
'fees', il.fees,
64
'discount', il.discount,
65
'taxable_amount', il.taxable_amount,
66
'sgst_amount', il.sgst_amount,
67
'cgst_amount', il.cgst_amount,
68
'igst_amount', il.igst_amount,
69
'total_amount', il.total_amount,
70
'serial_number', il.serial_number
71
) ORDER BY il.serial_number) AS invoice_lines,
72
ih.type,
73
ih.current_approval_level,
74
ih.is_created_by_scheduler,
75
ih.created_on_utc::timestamp with time zone,
76
ih.modified_on_utc::timestamp with time zone,
77
ih.created_by,
78
ih.modified_by
79
FROM invoice_headers ih
80
LEFT JOIN public.get_customer_details(ih.customer_id) c ON TRUE
81
LEFT JOIN public.get_warehouse_details(ih.destination_warehouse_id) wd ON TRUE
82
LEFT JOIN public.get_invoice_lines(ih.id, ih.invoice_status_id) il ON TRUE
83
JOIN public.invoice_statuses invst ON ih.invoice_status_id = invst.id
84
WHERE ih.id = p_invoice_header_id
85
GROUP BY
86
ih.id, ih.invoice_number, ih.credit_account_id, ih.invoice_voucher, ih.invoice_date,
87
ih.payment_term, ih.customer_id, c.name, c.email, c.mobile_number,
88
c.phone_number, c.gstin, c.short_name, c.pan, c.tan, ih.due_date,
89
ih.total_amount, ih.taxable_amount, ih.fees, ih.cgst_amount, ih.sgst_amount,
90
ih.igst_amount, ih.settled_amount, ih.currency_id, ih.invoice_status_id, invst.name, ih.discount, ih.note,
91
ih.round_off, ih.source_warehouse_id, wd.id, wd.customer_id, wd.name,
92
wd.address_id, wd.country_name, wd.country_id, wd.state_name, wd.state_id,
93
wd.city_name, wd.city_id, wd.address_line1, wd.address_line2, wd.zip_code,
94
wd.gstin,ih.type, ih.current_approval_level, ih.is_created_by_scheduler, ih.created_on_utc, ih.modified_on_utc, ih.created_by, ih.modified_by;
95
--order by il.serial_number;
96
97
ELSE
98
RETURN QUERY
99
SELECT
100
dih.id,
101
dih.invoice_number,
102
dih.credit_account_id,
103
dih.invoice_voucher,
104
dih.invoice_date::timestamp with time zone,
105
dih.payment_term,
106
dih.customer_id,
107
c.name AS customer_name,
108
c.email AS customer_email,
109
c.mobile_number AS customer_mobile_number,
110
c.phone_number AS customer_phone_number,
111
c.gstin AS customer_gstin,
112
c.short_name AS customer_short_name,
113
c.pan AS customer_pan,
114
c.tan AS customer_tan,
115
dih.due_date::timestamp with time zone,
116
dih.total_amount,
117
dih.taxable_amount,
118
dih.fees,
119
dih.cgst_amount,
120
dih.sgst_amount,
121
dih.igst_amount,
122
dih.settled_amount,
123
dih.currency_id,
124
dih.invoice_status_id,
125
dinvst.name :: text,
126
dih.discount,
127
dih.note,
128
dih.round_off,
129
dih.source_warehouse_id,
130
wd.id AS destination_warehouse_id,
131
wd.customer_id AS destination_customer_id,
132
wd.name AS destination_warehouse_name,
133
wd.address_id AS destination_address_id,
134
wd.country_name AS destination_country_name,
135
wd.country_id AS destination_country_id,
136
wd.state_name AS destination_state_name,
137
wd.state_id AS destination_state_id,
138
wd.city_name AS destination_city_name,
139
wd.city_id AS destination_city_id,
140
wd.address_line1 AS destination_address_line1,
141
wd.address_line2 AS destination_address_line2,
142
wd.zip_code AS destination_zip_code,
143
wd.gstin AS destination_gstin,
144
jsonb_agg(jsonb_build_object(
145
'invoice_line_id', il.id,
146
'product_id', il.product_id,
147
'price', il.price,
148
'quantity', il.quantity,
149
'fees', il.fees,
150
'discount', il.discount,
151
'taxable_amount', il.taxable_amount,
152
'sgst_amount', il.sgst_amount,
153
'cgst_amount', il.cgst_amount,
154
'igst_amount', il.igst_amount,
155
'total_amount', il.total_amount,
156
'serial_number', il.serial_number
157
) ORDER BY il.serial_number) AS invoice_lines,
158
dih.type,
159
dih.current_approval_level,
160
dih.is_created_by_scheduler,
161
dih.created_on_utc::timestamp with time zone,
162
dih.modified_on_utc::timestamp with time zone,
163
dih.created_by,
164
dih.modified_by
165
FROM draft_invoice_headers dih
166
LEFT JOIN public.get_customer_details(dih.customer_id) c ON TRUE
167
LEFT JOIN public.get_warehouse_details(dih.destination_warehouse_id) wd ON TRUE
168
LEFT JOIN public.get_invoice_lines(dih.id, dih.invoice_status_id) il ON TRUE
169
JOIN public.invoice_statuses dinvst ON dih.invoice_status_id = dinvst.id
170
WHERE dih.id = p_invoice_header_id
171
GROUP BY
172
dih.id, dih.invoice_number, dih.credit_account_id, dih.invoice_voucher, dih.invoice_date,
173
dih.payment_term, dih.customer_id, c.name, c.email, c.mobile_number,
174
c.phone_number, c.gstin, c.short_name, c.pan, c.tan, dih.due_date,
175
dih.total_amount, dih.taxable_amount, dih.fees, dih.cgst_amount, dih.sgst_amount,
176
dih.igst_amount, dih.settled_amount, dih.currency_id, dih.invoice_status_id, dinvst.name, dih.discount, dih.note,
177
dih.round_off, dih.source_warehouse_id, wd.id, wd.customer_id, wd.name,
178
wd.address_id, wd.country_name, wd.country_id, wd.state_name, wd.state_id,
179
wd.city_name, wd.city_id, wd.address_line1, wd.address_line2, wd.zip_code,
180
wd.gstin,dih.type, dih.current_approval_level, dih.is_created_by_scheduler, dih.created_on_utc, dih.modified_on_utc, dih.created_by, dih.modified_by;
181
--order by il.serial_number;
182
END IF;
183
END;
184
$function$
|
|||||
| Function | fetch_all_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.fetch_all_invoices(company_id uuid, fin_year_id integer)
2
RETURNS TABLE(invoice_id uuid, invoice_number text, credit_account_id uuid, invoice_voucher text, customer_id uuid, customer_name text, due_date date, invoice_date date, payment_term text, total_amount numeric, setteled_amount numeric, currency_id uuid, invoice_status text, invoice_status_id uuid, discount numeric, note text, created_on timestamp without time zone, modified_on timestamp without time zone, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name text, invoice_lines jsonb, invoice_type text, is_created_by_scheduler boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
financial_year_start DATE := MAKE_DATE(fin_year_id, 4, 1);
7
financial_year_end DATE := MAKE_DATE(fin_year_id + 1, 3, 31);
8
BEGIN
9
RETURN QUERY
10
SELECT
11
ih.id AS invoice_id,
12
ih.invoice_number,
13
ih.credit_account_id,
14
ih.invoice_voucher,
15
ih.customer_id,
16
(SELECT c.name FROM customers c WHERE c.id = ih.customer_id) AS customer_name,
17
ih.due_date,
18
ih.invoice_date,
19
ih.payment_term,
20
ih.total_amount,
21
ih.settled_amount,
22
ih.currency_id,
23
(SELECT ist.name FROM invoice_statuses ist WHERE ist.id = ih.invoice_status_id) AS invoice_status,
24
ih.invoice_status_id,
25
ih.discount,
26
ih.note,
27
ih.created_on_utc AS created_on,
28
ih.modified_on_utc AS modified_on,
29
ih.created_by,
30
ih.modified_by,
31
ih.source_warehouse_id,
32
ih.destination_warehouse_id,
33
(SELECT w.name FROM warehouses w WHERE w.id = ih.destination_warehouse_id) AS destination_warehouse_name,
34
(SELECT JSONB_AGG(
35
JSONB_BUILD_OBJECT(
36
'id', id,
37
'product_id', product_id,
38
'price', price,
39
'quantity', quantity,
40
'fees', fees,
41
'discount', discount,
42
'taxable_amount', taxable_amount,
43
'sgst_amount', sgst_amount,
44
'cgst_amount', cgst_amount,
45
'igst_amount', igst_amount,
46
'total_amount', total_amount,
47
'serial_number', serial_number
48
)
49
)
50
FROM invoice_details
51
WHERE invoice_header_id = ih.id) AS invoice_lines,
52
ih.type AS invoice_type,
53
ih.is_created_by_scheduler
54
FROM invoice_headers ih
55
WHERE ih.company_id = company_id
56
AND ih.invoice_date BETWEEN financial_year_start AND financial_year_end
57
58
UNION ALL
59
60
SELECT
61
dih.id AS invoice_id,
62
dih.invoice_number,
63
dih.credit_account_id,
64
dih.invoice_voucher,
65
dih.customer_id,
66
(SELECT c.name FROM customers c WHERE c.id = dih.customer_id) AS customer_name,
67
dih.due_date,
68
dih.invoice_date,
69
dih.payment_term,
70
dih.total_amount,
71
dih.settled_amount,
72
dih.currency_id,
73
(SELECT ist.name FROM invoice_statuses ist WHERE ist.id = dih.invoice_status_id) AS invoice_status,
74
dih.invoice_status_id,
75
dih.discount,
76
dih.note,
77
dih.created_on_utc AS created_on,
78
dih.modified_on_utc AS modified_on,
79
dih.created_by,
80
dih.modified_by,
81
dih.source_warehouse_id,
82
dih.destination_warehouse_id,
83
(SELECT w.name FROM warehouses w WHERE w.id = dih.destination_warehouse_id) AS destination_warehouse_name,
84
(SELECT JSONB_AGG(
85
JSONB_BUILD_OBJECT(
86
'id', id,
87
'product_id', product_id,
88
'price', price,
89
'quantity', quantity,
90
'fees', fees,
91
'discount', discount,
92
'taxable_amount', taxable_amount,
93
'sgst_amount', sgst_amount,
94
'cgst_amount', cgst_amount,
95
'igst_amount', igst_amount,
96
'total_amount', total_amount,
97
'serial_number', serial_number
98
)
99
)
100
FROM draft_invoice_details
101
WHERE invoice_header_id = dih.id) AS invoice_lines,
102
dih.type AS invoice_type,
103
dih.is_created_by_scheduler
104
FROM draft_invoice_headers dih
105
WHERE dih.company_id = company_id
106
AND dih.invoice_date BETWEEN financial_year_start AND financial_year_end;
107
END;
108
$function$
|
|||||
| Function | apply_penalty_for_due_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.apply_penalty_for_due_invoices(p_product_id uuid, p_company_id uuid, p_penalty_receivable_account_id uuid, p_revenue_account_id uuid, p_created_by uuid, p_batch_size integer DEFAULT 10, p_penalty_date date DEFAULT CURRENT_DATE)
2
RETURNS TABLE(invoice_header_id uuid, credit_account uuid, debit_account uuid, company_id uuid, customer_id uuid, invoice_number text, penalty_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
cursor_invoice CURSOR FOR
7
SELECT
8
ih.id AS invoice_header_id,
9
ih.total_amount,
10
ih.settled_amount,
11
ih.customer_id,
12
ih.invoice_number,
13
ih.due_date,
14
pc.percentage as percentage,
15
pf.name AS frequency
16
FROM
17
invoice_headers ih
18
JOIN
19
penalty_configs pc ON ih.penalty_config_id = pc.id
20
JOIN
21
penalty_frequencies pf ON pc.penalty_frequency_id = pf.id
22
LEFT JOIN
23
penalty_processing_logs pl ON ih.id = pl.invoice_id AND pl.processing_date = p_penalty_date
24
WHERE
25
ih.company_id = p_company_id
26
AND public.check_penalty_date(ih.due_date::date, p_penalty_date) -- Apply penalty one day after the due date
27
AND ih.invoice_status_id IN (3, 4)-- Approved or Partially Paid
28
AND (pl.invoice_id IS NULL OR pl.status = 'failed'); -- Only process unprocessed or failed ones
29
30
invoice RECORD;
31
penalty_amount NUMERIC;
32
frequency_factor NUMERIC;
33
base_amount NUMERIC;
34
batch_count INT := 0;
35
invoice_header_id uuid;
36
invoice_details_id uuid;
37
38
BEGIN
39
-- Create a temporary table to store results
40
CREATE TEMP TABLE temp_penalty_results (
41
invoice_header_id UUID,
42
credit_account UUID,
43
debit_account UUID,
44
company_id UUID,
45
customer_id UUID,
46
invoice_number TEXT,
47
penalty_amount NUMERIC
48
) ON COMMIT DROP;
49
50
OPEN cursor_invoice;
51
52
LOOP
53
-- Begin a new transaction batch
54
BEGIN
55
-- Fetch the first record in the current batch
56
FETCH cursor_invoice INTO invoice;
57
58
EXIT WHEN NOT FOUND; -- Exit if no more records to process
59
60
-- Process up to p_batch_size records within this transaction
61
FOR i IN 1 .. p_batch_size LOOP
62
-- Exit the inner loop if there are no more records
63
EXIT WHEN NOT FOUND;
64
65
BEGIN
66
-- Track the penalty application start
67
INSERT INTO penalty_processing_logs (invoice_id, processing_date, status)
68
VALUES (invoice.invoice_header_id, p_penalty_date, 'pending')
69
ON CONFLICT (invoice_id, processing_date) DO UPDATE SET status = 'pending';
70
71
-- Calculate penalty amount
72
base_amount := CASE
73
WHEN invoice.settled_amount IS NOT NULL THEN (invoice.total_amount - invoice.settled_amount)
74
ELSE invoice.total_amount
75
END;
76
RAISE NOTICE 'base_amount : %', base_amount;
77
78
frequency_factor := CASE invoice.frequency
79
WHEN 'Yearly' THEN (invoice.percentage / 100) / 12
80
WHEN 'Monthly' THEN (invoice.percentage / 100)
81
ELSE 0
82
END;
83
RAISE NOTICE 'frequency_factor : %', frequency_factor;
84
85
penalty_amount := ROUND(base_amount * frequency_factor);
86
RAISE NOTICE 'penalty_amount : %', penalty_amount;
87
88
SELECT gen_random_uuid() INTO invoice_details_id;
89
SELECT gen_random_uuid() INTO invoice_header_id;
90
-- Insert penalty into invoice_details
91
92
--insert into invoice_headers
93
CALL public.create_invoice_header(
94
invoice_header_id,
95
p_company_id,
96
invoice.customer_id,
97
p_penalty_receivable_account_id,
98
p_revenue_account_id,
99
p_penalty_date,
100
invoice.due_date::date,
101
0,
102
penalty_amount,
103
0,
104
0,
105
0,
106
penalty_amount,
107
0,
108
0,
109
0,
110
1,
111
'penalty charges',
112
3,
113
p_created_by,
114
'inv',
115
'00000000-0000-0000-0000-000000000000',
116
'00000000-0000-0000-0000-000000000000',
117
null,
118
null,
119
5
120
);
121
122
--insert into invoice_details
123
CALL public.create_invoice_detail(
124
invoice_header_id,
125
penalty_amount,
126
1,
127
0,
128
0,
129
0,
130
0,
131
0,
132
penalty_amount,
133
penalty_amount,
134
1,
135
p_product_id
136
);
137
138
RAISE NOTICE 'successful inserted in invoiceDetails';
139
140
-- Mark the penalty application as completed in the log
141
UPDATE penalty_processing_logs
142
SET status = 'completed'
143
WHERE invoice_id = invoice.invoice_header_id;
144
RAISE NOTICE 'successful updated in penalty_processing_logs';
145
146
-- Insert data into the temporary table for returning at the end
147
INSERT INTO temp_penalty_results (
148
invoice_header_id, credit_account, debit_account, company_id, customer_id, invoice_number, penalty_amount
149
) VALUES (
150
invoice.invoice_header_id, p_revenue_account_id, p_penalty_receivable_account_id, p_company_id,
151
invoice.customer_id, invoice.invoice_number, penalty_amount
152
);
153
154
-- Fetch the next invoice for processing within the same batch
155
FETCH cursor_invoice INTO invoice;
156
EXCEPTION
157
WHEN OTHERS THEN
158
-- Mark the failed invoices as 'failed' in the log
159
UPDATE penalty_processing_logs
160
SET status = 'failed'
161
WHERE invoice_id = invoice.invoice_header_id
162
AND processing_date = p_penalty_date;
163
164
-- Log the error
165
RAISE NOTICE 'Error in processing invoice %: %', invoice.invoice_header_id, SQLERRM;
166
END;
167
END LOOP;
168
169
-- Increment batch count for reference
170
batch_count := batch_count + 1;
171
END;
172
END LOOP;
173
174
CLOSE cursor_invoice;
175
176
-- Return all data from the temporary table at the end
177
RETURN QUERY SELECT * FROM temp_penalty_results;
178
END;
179
$function$
|
|||||
| Function | get_all_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_invoices(company_id uuid, fin_year_id integer)
2
RETURNS TABLE(invoice_id uuid, invoice_number character varying, credit_account_id uuid, invoice_voucher character varying, customer_id uuid, customer_name character varying, due_date date, invoice_date date, payment_term character varying, total_amount numeric, settled_amount numeric, currency_id integer, invoice_status character varying, invoice_status_id integer, discount numeric, note character varying, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name character varying, invoice_lines jsonb, type character varying, is_created_by_scheduler boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
ih.id AS invoice_id,
9
ih.invoice_number::character varying, -- Cast to character varying
10
ih.credit_account_id,
11
ih.invoice_voucher::character varying, -- Cast to character varying
12
ih.customer_id,
13
(SELECT c.name::character varying FROM customers c WHERE c.id = ih.customer_id) AS customer_name,
14
ih.due_date::date, -- Cast to date
15
ih.invoice_date::date, -- Cast to date
16
ih.payment_term::character varying, -- Cast to character varying
17
ih.total_amount,
18
ih.settled_amount,
19
ih.currency_id,
20
(SELECT s.name::character varying FROM invoice_statuses s WHERE s.id = ih.invoice_status_id) AS invoice_status,
21
ih.invoice_status_id, -- Already integer
22
ih.discount,
23
ih.note::character varying, -- Cast to character varying
24
ih.created_on_utc,
25
ih.modified_on_utc,
26
ih.created_by,
27
ih.modified_by,
28
ih.source_warehouse_id,
29
ih.destination_warehouse_id,
30
(SELECT w.name::character varying FROM warehouses w WHERE w.id = ih.destination_warehouse_id) AS destination_warehouse_name,
31
(
32
SELECT jsonb_agg(
33
jsonb_build_object(
34
'id', d.id,
35
'product_id', d.product_id,
36
'price', d.price,
37
'quantity', d.quantity,
38
'fees', d.fees,
39
'discount', d.discount,
40
'taxable_amount', d.taxable_amount,
41
'sgst_amount', d.sgst_amount,
42
'cgst_amount', d.cgst_amount,
43
'igst_amount', d.igst_amount,
44
'total_amount', d.total_amount,
45
'serial_number', d.serial_number
46
)
47
)
48
FROM invoice_details d WHERE d.invoice_header_id = ih.id
49
) AS invoice_lines,
50
ih.type::character varying, -- Cast to character varying
51
ih.is_created_by_scheduler
52
FROM invoice_headers ih
53
WHERE ih.company_id = get_all_invoices.company_id
54
AND ih.invoice_date BETWEEN MAKE_DATE(get_all_invoices.fin_year_id, 4, 1) AND MAKE_DATE(get_all_invoices.fin_year_id + 1, 3, 31)
55
UNION ALL
56
SELECT
57
dih.id AS invoice_id,
58
dih.invoice_number::character varying, -- Cast to character varying
59
dih.credit_account_id,
60
dih.invoice_voucher::character varying, -- Cast to character varying
61
dih.customer_id,
62
(SELECT c.name::character varying FROM customers c WHERE c.id = dih.customer_id) AS customer_name,
63
dih.due_date::date, -- Cast to date
64
dih.invoice_date::date, -- Cast to date
65
dih.payment_term::character varying, -- Cast to character varying
66
dih.total_amount,
67
dih.settled_amount,
68
dih.currency_id,
69
(SELECT s.name::character varying FROM invoice_statuses s WHERE s.id = dih.invoice_status_id) AS invoice_status,
70
dih.invoice_status_id, -- Already integer
71
dih.discount,
72
dih.note::character varying, -- Cast to character varying
73
dih.created_on_utc,
74
dih.modified_on_utc,
75
dih.created_by,
76
dih.modified_by,
77
dih.source_warehouse_id,
78
dih.destination_warehouse_id,
79
(SELECT w.name::character varying FROM warehouses w WHERE w.id = dih.destination_warehouse_id) AS destination_warehouse_name,
80
(
81
SELECT jsonb_agg(
82
jsonb_build_object(
83
'id', d.id,
84
'product_id', d.product_id,
85
'price', d.price,
86
'quantity', d.quantity,
87
'fees', d.fees,
88
'discount', d.discount,
89
'taxable_amount', d.taxable_amount,
90
'sgst_amount', d.sgst_amount,
91
'cgst_amount', d.cgst_amount,
92
'igst_amount', d.igst_amount,
93
'total_amount', d.total_amount,
94
'serial_number', d.serial_number
95
)
96
)
97
FROM draft_invoice_details d WHERE d.invoice_header_id = dih.id
98
) AS invoice_lines,
99
dih.type::character varying, -- Cast to character varying
100
dih.is_created_by_scheduler
101
FROM draft_invoice_headers dih
102
WHERE dih.company_id = get_all_invoices.company_id
103
AND dih.invoice_date BETWEEN MAKE_DATE(get_all_invoices.fin_year_id, 4, 1) AND MAKE_DATE(get_all_invoices.fin_year_id + 1, 3, 31);
104
END;
105
$function$
|
|||||
| Function | get_grouped_invoice_header | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_header(p_company_id uuid, p_fin_year_id integer)
2
RETURNS TABLE(id uuid, group_invoice_number text, grouped_invoice_template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text, group_invoice_batch_id text, paid_count integer, total_paid_amount numeric, original_total_amount numeric, remaining_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
7
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
8
BEGIN
9
RETURN QUERY
10
SELECT
11
gih.id,
12
gih.group_invoice_number,
13
gih.group_invoice_template_id,
14
gih.execution_date,
15
gih.error_message,
16
gih.success_count,
17
gih.total_count,
18
templates.invoice_description,
19
gih.group_invoice_batch_id,
20
21
/* Paid invoice count (fully settled invoices only) */
22
CAST(
23
COALESCE(
24
SUM(
25
CASE
26
WHEN ih.settled_amount >= ih.total_amount THEN 1
27
ELSE 0
28
END
29
),
30
0
31
) AS integer
32
) AS paid_count,
33
34
/* ✅ FIX: Cap payment per invoice */
35
COALESCE(
36
SUM(
37
LEAST(ih.settled_amount, ih.total_amount)
38
),
39
0
40
) AS total_paid_amount,
41
42
/* Original invoice total */
43
COALESCE(
44
SUM(ih.total_amount),
45
0
46
) AS original_total_amount,
47
48
/* ✅ FIX: Never negative remaining */
49
COALESCE(
50
SUM(
51
GREATEST(ih.total_amount - ih.settled_amount, 0)
52
),
53
0
54
) AS remaining_amount
55
56
FROM public.group_invoice_headers gih
57
INNER JOIN public.group_invoice_templates templates
58
ON templates.id = gih.group_invoice_template_id
59
60
LEFT JOIN public.group_invoice_details gid
61
ON gid.group_invoice_header_id = gih.id
62
AND gid.is_deleted = false
63
AND gid.company_id = p_company_id
64
65
LEFT JOIN public.invoice_headers ih
66
ON ih.id = gid.invoice_header_id
67
AND ih.is_deleted = false
68
AND ih.company_id = p_company_id
69
70
WHERE gih.company_id = p_company_id
71
AND gih.execution_date::date BETWEEN v_start_date AND v_end_date
72
73
GROUP BY
74
gih.id,
75
gih.group_invoice_number,
76
gih.group_invoice_template_id,
77
gih.execution_date,
78
gih.error_message,
79
gih.success_count,
80
gih.total_count,
81
templates.invoice_description,
82
gih.group_invoice_batch_id
83
84
ORDER BY gih.execution_date DESC;
85
END;
86
$function$
|
|||||
| Function | get_grouped_invoice_details | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_details(p_group_invoice_header_id uuid)
2
RETURNS TABLE(invoice_header_id uuid, invoice_number text, invoice_date timestamp without time zone, payment_term numeric, due_date timestamp without time zone, total_amount numeric, settled_amount numeric, taxable_amount numeric, discount numeric, sgst_amount numeric, cgst_amount numeric, igst_amount numeric, round_off numeric, note text, customer_id uuid, customer_name text, invoice_status_id integer, invoice_status text, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, credit_account_id uuid, invoice_voucher text, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name text, type integer, is_created_by_scheduler boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
ih.id AS invoice_header_id,
9
ih.invoice_number::text AS invoice_number, -- Explicit cast
10
ih.invoice_date,
11
ih.payment_term::numeric AS payment_term, -- Cast to numeric
12
ih.due_date,
13
ih.total_amount,
14
ih.settled_amount,
15
ih.taxable_amount,
16
ih.discount,
17
ih.sgst_amount,
18
ih.cgst_amount,
19
ih.igst_amount,
20
ih.round_off,
21
ih.note::text AS note, -- Explicit cast
22
ih.customer_id,
23
COALESCE(c.name, 'Unknown')::text AS customer_name, -- Explicit cast
24
ih.invoice_status_id,
25
COALESCE(s.name, 'Unknown')::text AS invoice_status, -- Explicit cast
26
ih.currency_id,
27
ih.created_on_utc,
28
ih.modified_on_utc,
29
ih.credit_account_id,
30
ih.invoice_voucher::text AS invoice_voucher, -- Explicit cast
31
ih.created_by,
32
ih.modified_by,
33
ih.source_warehouse_id,
34
ih.destination_warehouse_id,
35
COALESCE(w.name, 'Unknown')::text AS destination_warehouse_name, -- Explicit cast
36
ih.type,
37
ih.is_created_by_scheduler
38
FROM
39
public.group_invoice_details aid
40
INNER JOIN
41
invoice_headers ih ON aid.invoice_header_id = ih.id
42
LEFT JOIN
43
customers c ON ih.customer_id = c.id
44
LEFT JOIN
45
invoice_statuses s ON ih.invoice_status_id = s.id
46
LEFT JOIN
47
warehouses w ON ih.destination_warehouse_id = w.id
48
WHERE
49
aid.group_invoice_header_id = p_group_invoice_header_id
50
AND NOT aid.is_deleted
51
AND NOT ih.is_deleted;
52
END;
53
$function$
|
|||||
| Function | get_unit_dues_by_cust_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_unit_dues_by_cust_id(p_company_id uuid, p_customer_id uuid)
2
RETURNS TABLE(id uuid, name text, total_due numeric, total_paid numeric, original_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
6
BEGIN
7
RETURN QUERY
8
SELECT
9
c.id::uuid,
10
c.name::text,
11
COALESCE(SUM(
12
CASE
13
WHEN i.due_date < now()
14
AND i.is_deleted = false
15
AND (i.total_amount - i.settled_amount) > 0
16
THEN (i.total_amount - i.settled_amount)
17
ELSE 0
18
END
19
), 0)::numeric AS total_due,
20
COALESCE(SUM(
21
CASE
22
WHEN i.due_date < now()
23
AND i.is_deleted = false
24
THEN i.settled_amount
25
ELSE 0
26
END
27
), 0)::numeric AS total_paid,
28
COALESCE(SUM(i.total_amount), 0)::numeric AS original_amount
29
FROM public.customers c
30
LEFT JOIN public.invoice_headers i
31
ON i.customer_id = c.id AND i.company_id = p_company_id
32
WHERE c.is_deleted = false
33
AND c.company_id = p_company_id
34
AND c.id = p_customer_id
35
GROUP BY c.id, c.name;
36
END;
37
$function$
|
|||||
| Function | get_all_customers_details | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_customers_details(p_company_id uuid)
2
RETURNS TABLE(id uuid, name character varying, contact_name text, gst_in text, mobile_number text, email text, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, billing_address jsonb, contact_count integer)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
c.id,
9
c.name,
10
con.first_name || ' ' || con.last_name AS contact_name,
11
c.gstin AS gst_in,
12
con.mobile_number,
13
con.email,
14
c.created_by,
15
u_created.first_name || ' ' || u_created.last_name AS created_name,
16
c.modified_by,
17
u_modified.first_name || ' ' || u_modified.last_name AS modified_name,
18
c.created_on_utc,
19
c.modified_on_utc,
20
CASE
21
WHEN ba.id IS NOT NULL
22
THEN jsonb_build_object(
23
'AddressLine1', ba.address_line1,
24
'AddressLine2', ba.address_line2,
25
'ZipCode', ba.zip_code,
26
'CountryId', ba.country_id,
27
'StateId', ba.state_id,
28
'CityId', ba.city_id
29
)
30
ELSE NULL
31
END AS billing_address,
32
(SELECT COUNT(*)::integer -- Cast to integer
33
FROM public.customer_contacts cc_count
34
WHERE cc_count.customer_id = c.id) AS contact_count -- Contact count subquery
35
FROM
36
public.customers c
37
LEFT JOIN
38
public.customer_contacts cc ON c.id = cc.customer_id
39
LEFT JOIN
40
public.contacts con ON cc.contact_id = con.id And con.is_primary = true
41
LEFT JOIN
42
public.users u_created ON c.created_by = u_created.id
43
LEFT JOIN
44
public.users u_modified ON c.modified_by = u_modified.id
45
LEFT JOIN
46
public.addresses ba ON c.billing_address_id = ba.id
47
WHERE
48
c.company_id = p_company_id
49
AND c.is_deleted = false;
50
--AND con.is_primary = true;
51
END;
52
$function$
|
|||||
| Function | get_customer_by_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_customer_by_id(p_customer_id uuid)
2
RETURNS TABLE(id uuid, name text, gstin text, short_name text, pan text, tan text, proprietor_name text, outstanding_limit numeric, is_non_work boolean, interest_percentage numeric, billing_address_id uuid, billing_address jsonb, shipping_address_id uuid, shipping_address jsonb, bank_accounts jsonb, contacts jsonb, contact jsonb)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
c.id,
9
c.name::text, -- Cast to text
10
c.gstin::text, -- Cast to text
11
c.short_name::text, -- Cast to text
12
c.pan::text, -- Cast to text
13
c.tan::text, -- Cast to text
14
c.proprietor_name::text, -- Cast to text
15
c.outstanding_limit,
16
c.is_non_work,
17
COALESCE(c.interest_percentage, 0) AS interest_percentage, -- Ensure default 0 if NULL
18
c.billing_address_id,
19
20
-- Billing address as JSONB
21
CASE
22
WHEN ba.id IS NOT NULL THEN jsonb_build_object(
23
'AddressLine1', ba.address_line1,
24
'AddressLine2', ba.address_line2,
25
'ZipCode', ba.zip_code,
26
'CountryId', ba.country_id,
27
'StateId', ba.state_id,
28
'StateName', COALESCE(s.name, ''), -- Ensure empty string if NULL
29
'CityId', ba.city_id,
30
'CityName', COALESCE(ct.name, '') -- Ensure empty string if NULL
31
)
32
ELSE NULL
33
END AS billing_address,
34
35
c.shipping_address_id,
36
37
-- Shipping address as JSONB
38
CASE
39
WHEN sa.id IS NOT NULL THEN jsonb_build_object(
40
'AddressLine1', sa.address_line1,
41
'AddressLine2', sa.address_line2,
42
'ZipCode', sa.zip_code,
43
'CountryId', sa.country_id,
44
'StateId', sa.state_id,
45
'StateName', COALESCE(s2.name, ''), -- Ensure empty string if NULL
46
'CityId', sa.city_id,
47
'CityName', COALESCE(ct2.name, '') -- Ensure empty string if NULL
48
)
49
ELSE NULL
50
END AS shipping_address,
51
52
-- Bank accounts as JSONB array
53
COALESCE(
54
(
55
SELECT jsonb_agg(
56
jsonb_build_object(
57
'Id', ba.id,
58
'BankId', ba.bank_id,
59
'BranchName', ba.branch_name,
60
'BankName', b.name,
61
'AccountNumber', ba.account_number,
62
'IFSC', ba.ifsc
63
)
64
)
65
FROM customer_bank_accounts cba
66
JOIN bank_accounts ba ON cba.bank_account_id = ba.id
67
JOIN banks b ON ba.bank_id = b.id
68
WHERE cba.customer_id = p_customer_id AND ba.is_deleted = false
69
),
70
'[]'::jsonb -- Ensure empty array if no results
71
) AS bank_accounts,
72
73
-- Customer contacts as JSONB array
74
COALESCE(
75
(
76
SELECT jsonb_agg(
77
jsonb_build_object(
78
'Id', con.id,
79
'Salutation', con.salutation,
80
'FirstName', con.first_name,
81
'LastName', con.last_name,
82
'Email', con.email,
83
'PhoneNumber', con.phone_number,
84
'MobileNumber', con.mobile_number,
85
'IsPrimary', con.is_primary
86
)
87
)
88
FROM customer_contacts cc
89
JOIN contacts con ON cc.contact_id = con.id
90
WHERE cc.customer_id = p_customer_id AND con.is_deleted = false
91
),
92
'[]'::jsonb -- Ensure empty array if no results
93
) AS contacts,
94
95
-- Primary contact as JSONB
96
COALESCE(
97
98
(
99
SELECT jsonb_build_object(
100
'Id', con.id,
101
'Salutation', con.salutation,
102
'FirstName', con.first_name,
103
'LastName', con.last_name,
104
'Email', con.email,
105
'PhoneNumber', con.phone_number,
106
'MobileNumber', con.mobile_number
107
)
108
FROM customer_contacts cc
109
JOIN contacts con ON cc.contact_id = con.id
110
WHERE cc.customer_id = p_customer_id AND con.is_primary = TRUE AND con.is_deleted = false
111
LIMIT 1
112
),
113
NULL
114
) AS contact
115
FROM customers c
116
LEFT JOIN addresses ba ON c.billing_address_id = ba.id
117
LEFT JOIN addresses sa ON c.shipping_address_id = sa.id
118
-- Joining states and cities for billing address
119
LEFT JOIN states s ON ba.state_id = s.id
120
LEFT JOIN cities ct ON ba.city_id = ct.id
121
-- Joining states and cities for shipping address
122
LEFT JOIN states s2 ON sa.state_id = s2.id
123
LEFT JOIN cities ct2 ON sa.city_id = ct2.id
124
WHERE c.id = p_customer_id AND c.is_deleted = false;
125
END;
126
$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_invoice_company_approvers | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_company_approvers(p_company_id uuid)
2
RETURNS TABLE(id integer, level1_user_id uuid, level2_user_id uuid, payment_approver_user_id uuid)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
1 AS id,
9
(ARRAY_AGG(ia.user_id) FILTER (WHERE ia.status_id = 8))[1] AS level1_user_id,
10
(ARRAY_AGG(ia.user_id) FILTER (WHERE ia.status_id = 9))[1] AS level2_user_id,
11
(ARRAY_AGG(ia.user_id) FILTER (WHERE ia.status_id = 5))[1] AS payment_approver_user_id
12
FROM invoice_approval_user_company ia
13
WHERE ia.company_id = p_company_id
14
AND ia.is_deleted = false;
15
END;
16
$function$
|
|||||
| Function | create_user | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.create_user(p_user_id uuid, p_email text, p_phone_number text, p_first_name text, p_last_name text, p_created_by uuid, p_company_id 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
email,
22
phone_number,
23
first_name,
24
last_name,
25
password_hash,
26
created_on_utc,
27
created_by,
28
company_id
29
) VALUES (
30
p_user_id, -- Generate a new UUID for the user ID
31
p_email, -- User email
32
p_phone_number, -- User phone number
33
p_first_name, -- User first name
34
p_last_name, -- Default empty last name
35
'', -- Default empty password hash
36
NOW(), -- Current timestamp
37
p_created_by, -- Created by
38
p_company_id -- Link to the provided company
39
);
40
41
-- Return the new user ID
42
RETURN p_user_id;
43
END;
44
$function$
|
|||||
| Function | get_account_total_amount_expense | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_total_amount_expense(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
BEGIN
10
-- Step 1: Get the 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_id;
15
16
-- Step 2: Check if the financial year exists
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_id;
19
END IF;
20
21
-- Step 3: Calculate the total amount for the specific account within the financial year
22
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_amount
23
FROM public.journal_entries je
24
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
25
WHERE th.company_id = p_company_id
26
AND je.account_id = p_account_id
27
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
28
AND je.is_deleted = FALSE;
29
30
-- Step 4: Return the total amount
31
RETURN v_total_amount;
32
END;
33
$function$
|
|||||
| Function | get_all_customers_details_byorg | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_customers_details_byorg(p_organization_id uuid)
2
RETURNS TABLE(id uuid, name character varying, contact_name text, gst_in text, mobile_number text, email text, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, billing_address jsonb, contact_count integer)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
c.id,
9
c.name,
10
con.first_name || ' ' || con.last_name AS contact_name,
11
c.gstin AS gst_in,
12
con.mobile_number,
13
con.email,
14
c.created_by,
15
u_created.first_name || ' ' || u_created.last_name AS created_name,
16
c.modified_by,
17
u_modified.first_name || ' ' || u_modified.last_name AS modified_name,
18
c.created_on_utc,
19
c.modified_on_utc,
20
CASE
21
WHEN ba.id IS NOT NULL
22
THEN jsonb_build_object(
23
'AddressLine1', ba.address_line1,
24
'AddressLine2', ba.address_line2,
25
'ZipCode', ba.zip_code,
26
'CountryId', ba.country_id,
27
'StateId', ba.state_id,
28
'CityId', ba.city_id
29
)
30
ELSE NULL
31
END AS billing_address,
32
(SELECT COUNT(*)::integer -- Cast to integer
33
FROM public.customer_contacts cc_count
34
WHERE cc_count.customer_id = c.id) AS contact_count -- Contact count subquery
35
FROM
36
public.customers c
37
JOIN
38
public.customer_contacts cc ON c.id = cc.customer_id
39
JOIN
40
public.contacts con ON cc.contact_id = con.id
41
LEFT JOIN
42
public.users u_created ON c.created_by = u_created.id
43
LEFT JOIN
44
public.users u_modified ON c.modified_by = u_modified.id
45
LEFT JOIN
46
public.addresses ba ON c.billing_address_id = ba.id
47
JOIN
48
public.companies comp ON c.company_id = comp.id -- Join companies table
49
WHERE
50
comp.organization_id = p_organization_id -- Filter by organization_id
51
AND c.is_deleted = false
52
AND con.is_primary = true;
53
END;
54
$function$
|
|||||
| Function | get_all_invoice_account_approvers | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_invoice_account_approvers(p_company_id uuid)
2
RETURNS TABLE(id integer, account_id uuid, status_id integer, user_id uuid, approval_level integer, required_approval_levels integer)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
ia.id,
9
ia.account_id,
10
ia.status_id, -- Use ia.status_id here (the user's approval status)
11
ia.user_id,
12
ia.approval_level,
13
iaal.approval_level_required AS required_approval_levels
14
FROM invoice_approval_users_account ia
15
JOIN invoice_account_approval_levels iaal
16
ON ia.company_id = iaal.company_id
17
AND ia.account_id = iaal.account_id
18
WHERE ia.company_id = p_company_id
19
AND ia.is_deleted = FALSE
20
AND iaal.is_deleted = FALSE
21
AND iaal.status_id = 2 -- Filter for status_id = 2
22
ORDER BY ia.account_id, ia.status_id;
23
END;
24
$function$
|
|||||
| Function | upsert_customer_default_account | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.upsert_customer_default_account(p_updates jsonb)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
update_record JSONB;
7
v_customer_id UUID;
8
v_account_id UUID;
9
v_company_id UUID;
10
v_user_id UUID;
11
BEGIN
12
FOR update_record IN SELECT * FROM jsonb_array_elements(p_updates)
13
LOOP
14
v_customer_id := update_record->>'CustomerId';
15
v_account_id := update_record->>'AccountId';
16
v_company_id := update_record->>'CompanyId';
17
v_user_id := update_record->>'UserId';
18
19
INSERT INTO customer_default_accounts (
20
customer_id, account_id, company_id, created_by, created_on_utc, is_deleted
21
)
22
VALUES (
23
v_customer_id, v_account_id, v_company_id, v_user_id, now(), false
24
)
25
ON CONFLICT (customer_id, company_id) DO UPDATE
26
SET
27
account_id = EXCLUDED.account_id,
28
modified_by = EXCLUDED.created_by,
29
modified_on_utc = now(),
30
is_deleted = false;
31
END LOOP;
32
END;
33
$function$
|
|||||
| Function | get_invoice_approval_log | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_approval_log(p_invoice_id uuid)
2
RETURNS TABLE(status integer, status_name text, next_status integer, next_status_name text, previous_status integer, previous_status_name text, is_initial boolean, approver_user_id uuid, approver_user_name text, approved_by uuid, approved_by_name text, approved_on timestamp without time zone, invoice_id uuid, sales_account uuid, created_by_name text, approval_level integer)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
IF EXISTS (SELECT 1 FROM public.invoice_headers WHERE id = p_invoice_id) THEN
7
RETURN QUERY
8
SELECT
9
iw.status,
10
cs.name::TEXT AS status_name,
11
iw.next_status,
12
ns.name::TEXT AS next_status_name,
13
iw.previous_status,
14
ps.name::TEXT AS previous_status_name,
15
iw.is_initial,
16
COALESCE(iaua.user_id, iauc.user_id) AS approver_user_id,
17
CONCAT(COALESCE(uaa.first_name, uca.first_name), ' ', COALESCE(uaa.last_name, uca.last_name))::TEXT AS approver_user_name,
18
ial.approved_by,
19
CONCAT(approved_by_user.first_name, ' ', approved_by_user.last_name)::TEXT AS approved_by_name,
20
ial.approved_on,
21
ih.id AS invoice_id,
22
ih.credit_account_id AS sales_account,
23
CONCAT(created_by_user.first_name, ' ', created_by_user.last_name)::TEXT AS created_by_name,
24
iw.approval_level
25
FROM invoice_workflow iw
26
JOIN invoice_headers ih ON ih.id = p_invoice_id
27
LEFT JOIN invoice_statuses cs ON cs.id = iw.status
28
LEFT JOIN invoice_statuses ns ON ns.id = iw.next_status
29
LEFT JOIN invoice_statuses ps ON ps.id = iw.previous_status
30
LEFT JOIN invoice_approval_user_company iauc
31
ON iauc.company_id = ih.company_id
32
AND iauc.status_id = iw.status
33
AND iauc.approval_level = iw.approval_level
34
LEFT JOIN invoice_approval_users_account iaua
35
ON iaua.company_id = ih.company_id
36
AND iaua.account_id = ih.credit_account_id
37
AND iaua.status_id = iw.status
38
AND iaua.approval_level = iw.approval_level
39
LEFT JOIN users uca ON uca.id = iauc.user_id
40
LEFT JOIN users uaa ON uaa.id = iaua.user_id
41
LEFT JOIN users created_by_user ON created_by_user.id = ih.created_by
42
LEFT JOIN LATERAL (
43
SELECT * FROM invoice_approval_logs log
44
WHERE log.invoice_id = ih.id
45
AND log.status_id = iw.status
46
AND log.approval_level = iw.approval_level
47
ORDER BY log.approved_on DESC
48
LIMIT 1
49
) ial ON true
50
LEFT JOIN users approved_by_user ON approved_by_user.id = ial.approved_by
51
WHERE iw.company_id = ih.company_id
52
AND iw.is_deleted = false
53
AND iw.is_enabled = true
54
ORDER BY iw.approval_level;
55
56
ELSE
57
RETURN QUERY
58
SELECT
59
iw.status,
60
cs.name::TEXT AS status_name,
61
iw.next_status,
62
ns.name::TEXT AS next_status_name,
63
iw.previous_status,
64
ps.name::TEXT AS previous_status_name,
65
iw.is_initial,
66
COALESCE(iaua.user_id, iauc.user_id) AS approver_user_id,
67
CONCAT(COALESCE(uaa.first_name, uca.first_name), ' ', COALESCE(uaa.last_name, uca.last_name))::TEXT AS approver_user_name,
68
NULL::UUID AS approved_by,
69
NULL::TEXT AS approved_by_name,
70
NULL::TIMESTAMP AS approved_on,
71
dih.id AS invoice_id,
72
dih.credit_account_id AS sales_account,
73
CONCAT(created_by_user.first_name, ' ', created_by_user.last_name)::TEXT AS created_by_name,
74
iw.approval_level
75
FROM invoice_workflow iw
76
JOIN draft_invoice_headers dih ON dih.id = p_invoice_id
77
LEFT JOIN invoice_statuses cs ON cs.id = iw.status
78
LEFT JOIN invoice_statuses ns ON ns.id = iw.next_status
79
LEFT JOIN invoice_statuses ps ON ps.id = iw.previous_status
80
LEFT JOIN invoice_approval_user_company iauc
81
ON iauc.company_id = dih.company_id
82
AND iauc.status_id = iw.status
83
AND iauc.approval_level = iw.approval_level
84
LEFT JOIN invoice_approval_users_account iaua
85
ON iaua.company_id = dih.company_id
86
AND iaua.account_id = dih.credit_account_id
87
AND iaua.status_id = iw.status
88
AND iaua.approval_level = iw.approval_level
89
LEFT JOIN users uca ON uca.id = iauc.user_id
90
LEFT JOIN users uaa ON uaa.id = iaua.user_id
91
LEFT JOIN users created_by_user ON created_by_user.id = dih.created_by
92
WHERE iw.company_id = dih.company_id
93
AND iw.is_deleted = false
94
AND iw.is_enabled = true
95
ORDER BY iw.approval_level;
96
END IF;
97
END;
98
$function$
|
|||||
| Function | get_invoice_lines | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_lines(p_invoice_header_id uuid, p_invoice_status_id integer)
2
RETURNS TABLE(id uuid, product_id uuid, price numeric, quantity numeric, fees numeric, discount numeric, taxable_amount numeric, sgst_amount numeric, cgst_amount numeric, igst_amount numeric, total_amount numeric, serial_number integer)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
IF p_invoice_status_id >= 3 THEN
7
RETURN QUERY
8
SELECT
9
ids.id,
10
ids.product_id,
11
ids.price,
12
ids.quantity,
13
ids.fees,
14
ids.discount,
15
ids.taxable_amount,
16
ids.sgst_amount,
17
ids.cgst_amount,
18
ids.igst_amount,
19
ids.total_amount,
20
ids.serial_number
21
FROM invoice_details ids
22
WHERE ids.invoice_header_id = p_invoice_header_id and ids.is_deleted = false
23
ORDER BY ids.serial_number;
24
ELSE
25
RETURN QUERY
26
SELECT
27
did.id,
28
did.product_id,
29
did.price,
30
did.quantity,
31
did.fees,
32
did.discount,
33
did.taxable_amount,
34
did.sgst_amount,
35
did.cgst_amount,
36
did.igst_amount,
37
did.total_amount,
38
did.serial_number
39
FROM draft_invoice_details did
40
WHERE did.invoice_header_id = p_invoice_header_id and did.is_deleted = false
41
ORDER BY did.serial_number;
42
END IF;
43
END;
44
$function$
|
|||||
| Function | get_new_draft_invoice_number | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_new_draft_invoice_number(p_company_id uuid, p_date date)
2
RETURNS character varying
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_finance_year integer;
7
new_invoice_id integer;
8
p_prefix varchar;
9
invoice_number varchar;
10
BEGIN
11
-- Determine the start year of the financial year based on p_date
12
IF EXTRACT(MONTH FROM p_date) >= 4 THEN
13
v_finance_year := EXTRACT(YEAR FROM p_date);
14
ELSE
15
v_finance_year := EXTRACT(YEAR FROM p_date) - 1;
16
END IF;
17
18
-- Attempt to update the last_invoice_id for the given company and financial year
19
WITH x AS (
20
UPDATE public.draft_invoice_header_ids
21
SET last_invoice_id = last_invoice_id + 1
22
WHERE company_id = p_company_id AND fin_year = v_finance_year
23
RETURNING last_invoice_id, invoice_prefix
24
),
25
insert_x AS (
26
-- If no rows were updated, insert a new row with the default prefix 'DIN' and default invoice_length
27
INSERT INTO public.draft_invoice_header_ids (company_id, fin_year, invoice_prefix, last_invoice_id, invoice_length)
28
SELECT p_company_id, v_finance_year, 'DIN', 1, 8 -- Default invoice_length set to 8
29
WHERE NOT EXISTS (SELECT 1 FROM x)
30
RETURNING last_invoice_id, invoice_prefix
31
)
32
-- Use COALESCE to return the correct last_invoice_id and invoice_prefix
33
SELECT COALESCE((SELECT last_invoice_id FROM x LIMIT 1), (SELECT last_invoice_id FROM insert_x LIMIT 1)),
34
COALESCE((SELECT invoice_prefix FROM x LIMIT 1), (SELECT invoice_prefix FROM insert_x LIMIT 1))
35
INTO new_invoice_id, p_prefix;
36
37
-- Concatenate the prefix and new_invoice_id to form the invoice number
38
invoice_number := p_prefix || LPAD(new_invoice_id::text, 4, '0');
39
40
-- Return the generated invoice number
41
RETURN invoice_number;
42
END;
43
$function$
|
|||||
| Function | bulk_delete_customers | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.bulk_delete_customers(p_customer_ids uuid[], p_modified_by uuid, p_deleted_on_utc timestamp without time zone)
2
RETURNS TABLE(id integer, customer_id uuid, status text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_id UUID;
7
v_blocked_count INT;
8
row_index INT := 1;
9
BEGIN
10
FOREACH v_id IN ARRAY p_customer_ids LOOP
11
-- 1. Skip if customer already deleted
12
IF NOT EXISTS (
13
SELECT 1 FROM public.customers c WHERE c.id = v_id AND c.is_deleted = false
14
) THEN
15
id := row_index;
16
bulk_delete_customers.customer_id := v_id;
17
bulk_delete_customers.status := 'Skipped - Customer Not Found or Already Deleted';
18
row_index := row_index + 1;
19
RETURN NEXT;
20
CONTINUE;
21
END IF;
22
23
-- 2. Check if any financial invoices exist
24
SELECT COUNT(*) INTO v_blocked_count
25
FROM (
26
SELECT 1 FROM public.invoice_headers ih
27
WHERE ih.customer_id = v_id AND ih.is_deleted = false AND ih.invoice_status_id IN (3, 4, 5)
28
UNION ALL
29
SELECT 1 FROM public.draft_invoice_headers dih
30
WHERE dih.customer_id = v_id AND dih.is_deleted = false AND dih.invoice_status_id IN (3, 4, 5)
31
) AS financial_invoices;
32
33
IF v_blocked_count > 0 THEN
34
id := row_index;
35
bulk_delete_customers.customer_id := v_id;
36
bulk_delete_customers.status := 'Blocked - Financial Invoices Exist (Approved/Partially Paid/Paid)';
37
row_index := row_index + 1;
38
RETURN NEXT;
39
CONTINUE;
40
END IF;
41
42
-- 3. Soft delete eligible draft invoice details
43
UPDATE public.draft_invoice_details did
44
SET is_deleted = true,
45
deleted_on_utc = p_deleted_on_utc
46
WHERE did.invoice_header_id IN (
47
SELECT dih.id FROM public.draft_invoice_headers dih
48
WHERE dih.customer_id = v_id AND dih.is_deleted = false AND dih.invoice_status_id NOT IN (3, 4, 5)
49
);
50
51
-- 4. Soft delete eligible draft invoice headers
52
UPDATE public.draft_invoice_headers dih
53
SET is_deleted = true,
54
modified_by = p_modified_by,
55
deleted_on_utc = p_deleted_on_utc
56
WHERE dih.customer_id = v_id AND dih.is_deleted = false AND dih.invoice_status_id NOT IN (3, 4, 5);
57
58
-- 5. Soft delete eligible final invoice details
59
UPDATE public.invoice_details id
60
SET is_deleted = true,
61
deleted_on_utc = p_deleted_on_utc
62
WHERE id.invoice_header_id IN (
63
SELECT ih.id FROM public.invoice_headers ih
64
WHERE ih.customer_id = v_id AND ih.is_deleted = false AND ih.invoice_status_id NOT IN (3, 4, 5)
65
);
66
67
-- 6. Soft delete eligible final invoice headers
68
UPDATE public.invoice_headers ih
69
SET is_deleted = true,
70
modified_by = p_modified_by,
71
deleted_on_utc = p_deleted_on_utc
72
WHERE ih.customer_id = v_id AND ih.is_deleted = false AND ih.invoice_status_id NOT IN (3, 4, 5);
73
74
-- 7. Soft delete customer
75
UPDATE public.customers c
76
SET is_deleted = true,
77
modified_by = p_modified_by,
78
deleted_on_utc = p_deleted_on_utc
79
WHERE c.id = v_id;
80
81
-- 8. Return success
82
id := row_index;
83
bulk_delete_customers.customer_id := v_id;
84
bulk_delete_customers.status := 'Deleted';
85
row_index := row_index + 1;
86
RETURN NEXT;
87
END LOOP;
88
END;
89
$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
BEGIN
10
-- Step 1: Get the 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_id;
15
16
-- Step 2: Check if the financial year exists
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_id;
19
END IF;
20
21
-- Step 3: Calculate the total amount for the specific account within the financial year
22
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_amount
23
FROM public.journal_entries je
24
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
25
WHERE th.company_id = p_company_id
26
AND je.account_id = p_account_id
27
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
28
AND je.is_deleted = FALSE;
29
30
-- Step 4: Return the total amount
31
RETURN v_total_amount;
32
END;
33
$function$
|
|||||
| Function | get_all_customer_notes | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_customer_notes(p_company_id uuid)
2
RETURNS TABLE(id uuid, company_id uuid, customer_id uuid, customer_name text, note_date date, invoice_id text, customer_note_status_id integer, customer_note_status text, is_debit_note boolean, total_amount numeric, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
cnh.id,
9
cnh.company_id,
10
cnh.customer_id,
11
c.name::TEXT AS customer_name, -- Cast to TEXT
12
cnh.note_date::DATE, -- Cast to DATE
13
cnh.invoice_id::TEXT,
14
cnh.customer_note_status_id,
15
cn_status.name AS customer_note_status,
16
cnh.is_debit_note,
17
cnh.total_amount,
18
cnh.created_on_utc,
19
cnh.modified_on_utc,
20
cnh.created_by,
21
(SELECT CONCAT(u.first_name, ' ', u.last_name) FROM users u WHERE u.id = cnh.created_by) AS created_by_name,
22
cnh.modified_by,
23
(SELECT CONCAT(u.first_name, ' ', u.last_name) FROM users u WHERE u.id = cnh.modified_by) AS modified_by_name
24
FROM customer_note_headers cnh
25
JOIN customers c ON cnh.customer_id = c.id
26
JOIN customer_note_statuses cn_status ON cnh.customer_note_status_id = cn_status.id
27
WHERE cnh.company_id = p_company_id;
28
END;
29
$function$
|
|||||
| Function | get_all_invoice_headers | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_invoice_headers(p_company_id uuid)
2
RETURNS TABLE(id uuid, invoice_number text, invoice_voucher text, customer_id uuid, customer_name text, due_date timestamp with time zone, invoice_date timestamp with time zone, payment_term integer, total_amount numeric, settled_amount numeric, discount numeric, note text, currency_id integer, invoice_status_id integer, invoice_status text, type integer, is_created_by_scheduler boolean, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
-- Query InvoiceHeaders
8
SELECT header.id, header.invoice_number, header.invoice_voucher, header.customer_id,
9
customer.name::text AS customer_name,
10
header.due_date::timestamptz, header.invoice_date::timestamptz, header.payment_term,
11
header.total_amount, header.settled_amount, header.discount, header.note,
12
header.currency_id, header.invoice_status_id, status.name::text AS invoice_status, header.type, header.is_created_by_scheduler,
13
header.created_by,
14
COALESCE(u_created.first_name || ' ' || u_created.last_name, 'N/A') AS created_by_name, -- full name or 'N/A'
15
header.modified_by,
16
COALESCE(u_modified.first_name || ' ' || u_modified.last_name, 'N/A') AS modified_by_name -- full name or 'N/A'
17
FROM invoice_headers header
18
JOIN customers customer ON customer.id = header.customer_id
19
JOIN invoice_statuses status ON status.id = header.invoice_status_id
20
LEFT JOIN users u_created ON u_created.id = header.created_by -- join to get the created_by user's full name
21
LEFT JOIN users u_modified ON u_modified.id = header.modified_by -- join to get the modified_by user's full name
22
WHERE header.company_id = p_company_id
23
AND header.is_deleted = false
24
25
UNION ALL
26
27
-- Query DraftInvoiceHeaders
28
SELECT draft.id, draft.invoice_number, draft.invoice_voucher, draft.customer_id,
29
customer.name::text AS customer_name,
30
draft.due_date::timestamptz, draft.invoice_date::timestamptz, draft.payment_term,
31
draft.total_amount, draft.settled_amount, draft.discount, draft.note,
32
draft.currency_id, draft.invoice_status_id, status.name::text AS invoice_status, draft.type, draft.is_created_by_scheduler,
33
draft.created_by,
34
COALESCE(u_created.first_name || ' ' || u_created.last_name, 'N/A') AS created_by_name, -- full name or 'N/A'
35
draft.modified_by,
36
COALESCE(u_modified.first_name || ' ' || u_modified.last_name, 'N/A') AS modified_by_name -- full name or 'N/A'
37
FROM draft_invoice_headers draft
38
JOIN customers customer ON customer.id = draft.customer_id
39
JOIN invoice_statuses status ON status.id = draft.invoice_status_id
40
LEFT JOIN users u_created ON u_created.id = draft.created_by -- join to get the created_by user's full name
41
LEFT JOIN users u_modified ON u_modified.id = draft.modified_by -- join to get the modified_by user's full name
42
WHERE draft.company_id = p_company_id
43
AND draft.is_deleted = false;
44
END;
45
$function$
|
|||||
| Function | get_group_invoice_stats | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_group_invoice_stats(p_group_invoice_header_id uuid)
2
RETURNS TABLE(paid_count integer, paid_amount numeric, unpaid_count integer, unpaid_amount numeric, overdue_count integer, overdue_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
-- Validate if the p_group_invoice_header_id belongs to a valid group invoice header
7
IF NOT EXISTS (
8
SELECT 1
9
FROM group_invoice_headers gih
10
WHERE gih.id = p_group_invoice_header_id
11
AND gih.is_deleted = false
12
) THEN
13
RAISE EXCEPTION 'Invalid group_invoice_header_id: It is either deleted or does not exist.';
14
END IF;
15
16
-- Calculate grouped invoice statistics
17
RETURN QUERY
18
SELECT
19
-- Paid
20
COALESCE(COUNT(CASE WHEN ih.invoice_status_id = 5 THEN 1 ELSE NULL END), 0)::INTEGER AS paid_count,
21
COALESCE(SUM(CASE WHEN ih.invoice_status_id = 5 THEN ih.total_amount ELSE 0 END), 0) AS paid_amount,
22
23
-- Unpaid (Draft, Pending Approval, Approved, or Partially Paid)
24
COALESCE(COUNT(CASE WHEN ih.invoice_status_id IN (1, 2, 3, 4) THEN 1 ELSE NULL END), 0)::INTEGER AS unpaid_count,
25
COALESCE(SUM(CASE WHEN ih.invoice_status_id IN (1, 2, 3, 4) THEN ih.total_amount ELSE 0 END), 0) AS unpaid_amount,
26
27
-- Overdue
28
COALESCE(COUNT(CASE WHEN ih.invoice_status_id NOT IN (5) AND ih.due_date < now() THEN 1 ELSE NULL END), 0)::INTEGER AS overdue_count,
29
COALESCE(SUM(CASE WHEN ih.invoice_status_id NOT IN (5) AND ih.due_date < now() THEN ih.total_amount ELSE 0 END), 0) AS overdue_amount
30
FROM group_invoice_details gid
31
INNER JOIN invoice_headers ih
32
ON ih.id = gid.invoice_header_id
33
WHERE gid.group_invoice_header_id = p_group_invoice_header_id
34
AND gid.is_deleted = false;
35
END;
36
$function$
|
|||||
| Function | get_invoice_workflow_config | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_workflow_config(p_company_id uuid)
2
RETURNS TABLE(id integer, name text, is_enabled boolean, next_statuses integer[], previous_statuses integer[])
3
LANGUAGE sql
4
AS $function$
5
SELECT
6
s.id,
7
s.name,
8
9
-- Dynamically determine if at least one transition from this status is enabled
10
COALESCE((
11
SELECT bool_or(w.is_enabled)
12
FROM invoice_workflow w
13
WHERE w.company_id = p_company_id
14
AND w.status = s.id
15
AND w.is_deleted = false
16
), false) AS is_enabled,
17
18
-- Forward links (next_statuses)
19
COALESCE(ARRAY(
20
SELECT w.next_status
21
FROM invoice_workflow w
22
WHERE w.company_id = p_company_id
23
AND w.status = s.id
24
AND w.is_deleted = false
25
AND w.is_enabled = true
26
ORDER BY w.next_status
27
), ARRAY[]::INT[]) AS next_statuses,
28
29
-- Backward links (previous_statuses)
30
COALESCE(ARRAY(
31
SELECT DISTINCT w.previous_status
32
FROM invoice_workflow w
33
WHERE w.company_id = p_company_id
34
AND w.status = s.id
35
AND w.is_deleted = false
36
AND w.is_enabled = true
37
AND w.previous_status IS NOT NULL
38
ORDER BY w.previous_status
39
), ARRAY[]::INT[]) AS previous_statuses
40
41
FROM invoice_statuses s
42
WHERE s.is_deleted = false;
43
$function$
|
|||||
| Function | get_new_invoice_number | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_new_invoice_number(p_company_id uuid, p_date date)
2
RETURNS character varying
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_finance_year integer;
7
new_invoice_id integer;
8
p_prefix varchar;
9
invoice_number varchar;
10
BEGIN
11
-- Determine the start year of the financial year based on p_date
12
IF EXTRACT(MONTH FROM p_date) >= 4 THEN
13
v_finance_year := EXTRACT(YEAR FROM p_date);
14
ELSE
15
v_finance_year := EXTRACT(YEAR FROM p_date) - 1;
16
END IF;
17
18
-- Attempt to update the last_invoice_id for the given company and financial year
19
WITH x AS (
20
UPDATE public.invoice_header_ids
21
SET last_invoice_id = last_invoice_id + 1
22
WHERE company_id = p_company_id AND fin_year = v_finance_year
23
RETURNING last_invoice_id, invoice_prefix
24
),
25
insert_x AS (
26
-- If no rows were updated, insert a new row with the default prefix 'INV' and default invoice_length
27
INSERT INTO public.invoice_header_ids (company_id, fin_year, invoice_prefix, last_invoice_id, invoice_length)
28
SELECT p_company_id, v_finance_year, 'INV', 1, 8 -- Default invoice_length set to 8
29
WHERE NOT EXISTS (SELECT 1 FROM x)
30
RETURNING last_invoice_id, invoice_prefix
31
)
32
-- Use COALESCE to return the correct last_invoice_id and invoice_prefix
33
SELECT COALESCE((SELECT last_invoice_id FROM x LIMIT 1), (SELECT last_invoice_id FROM insert_x LIMIT 1)),
34
COALESCE((SELECT invoice_prefix FROM x LIMIT 1), (SELECT invoice_prefix FROM insert_x LIMIT 1))
35
INTO new_invoice_id, p_prefix;
36
37
-- Concatenate the prefix and new_invoice_id to form the invoice number
38
invoice_number := p_prefix || LPAD(new_invoice_id::text, 4, '0');
39
40
-- Return the generated invoice number
41
RETURN invoice_number;
42
END;
43
$function$
|
|||||
| Function | update_invoice_next_status_main | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.update_invoice_next_status_main(p_company_id uuid, p_invoice_ids uuid[], p_modified_by uuid)
2
RETURNS TABLE(invoice_id uuid, status_name text, error_msg text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_error_message text;
7
v_invoice_ids_draft uuid[];
8
v_invoice_ids_pending uuid[];
9
v_invoice_ids_other uuid[];
10
v_next_temp_id int;
11
BEGIN
12
RAISE NOTICE 'START update_invoice_next_status_main: company_id=%, modified_by=%', p_company_id, p_modified_by;
13
RAISE NOTICE 'Incoming Invoice IDs: %', p_invoice_ids;
14
15
-- Clean up temp_invoice_next_status for these invoice IDs to avoid duplicate entries
16
DELETE FROM temp_invoice_next_status tinv
17
WHERE tinv.invoice_id = ANY(p_invoice_ids);
18
19
-- Classify invoices by current status
20
SELECT array_agg(id) INTO v_invoice_ids_draft
21
FROM public.draft_invoice_headers dih
22
WHERE dih.id = ANY(p_invoice_ids) AND dih.invoice_status_id = 1;
23
24
SELECT array_agg(id) INTO v_invoice_ids_pending
25
FROM public.draft_invoice_headers dih
26
WHERE dih.id = ANY(p_invoice_ids) AND dih.invoice_status_id = 2;
27
28
SELECT array_agg(id) INTO v_invoice_ids_other
29
FROM public.invoice_headers ih
30
WHERE ih.id = ANY(p_invoice_ids) AND ih.invoice_status_id >= 3;
31
32
RAISE NOTICE 'Draft invoices: %', COALESCE(v_invoice_ids_draft, '{}');
33
RAISE NOTICE 'Pending Approval invoices: %', COALESCE(v_invoice_ids_pending, '{}');
34
RAISE NOTICE 'Other (Approved or higher) invoices: %', COALESCE(v_invoice_ids_other, '{}');
35
36
BEGIN
37
-- Direct update draft invoices to Pending Approval
38
IF array_length(v_invoice_ids_draft, 1) > 0 THEN
39
RAISE NOTICE 'Directly updating Draft invoices to Pending Approval';
40
41
UPDATE public.draft_invoice_headers
42
SET invoice_status_id = 2,
43
modified_by = p_modified_by,
44
modified_on_utc = now()
45
WHERE id = ANY(v_invoice_ids_draft);
46
47
-- Insert approval logs
48
INSERT INTO public.invoice_approval_logs(
49
invoice_id, status_id, approved_by, approved_on, "comment",
50
created_on_utc, created_by, approval_level
51
)
52
SELECT
53
dh.id, 2, p_modified_by, now(),
54
'Direct update from Draft to Pending Approval',
55
now(), p_modified_by, 0
56
FROM public.draft_invoice_headers dh
57
WHERE dh.id = ANY(v_invoice_ids_draft);
58
59
-- Generate next id for temp_invoice_next_status
60
SELECT COALESCE(MAX(id), 0) INTO v_next_temp_id FROM temp_invoice_next_status;
61
62
-- Insert into temp_invoice_next_status
63
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
64
SELECT
65
row_number() OVER () + v_next_temp_id AS id,
66
dh.id,
67
'Pending Approval',
68
NULL
69
FROM public.draft_invoice_headers dh
70
WHERE dh.id = ANY(v_invoice_ids_draft);
71
72
END IF;
73
74
-- Call update_draft_invoice_next_status if Pending Approval invoices found and p_invoice_status_id = 2
75
IF array_length(v_invoice_ids_pending, 1) > 0 THEN
76
RAISE NOTICE 'Calling update_draft_invoice_next_status for Pending Approval invoices';
77
CALL public.update_draft_invoice_next_status(p_company_id, v_invoice_ids_pending, p_modified_by);
78
END IF;
79
80
-- Call update_invoice_next_status_for_invoice_header for Approved or higher invoices
81
IF array_length(v_invoice_ids_other, 1) > 0 THEN
82
RAISE NOTICE 'Calling update_invoice_next_status_for_invoice_header for Approved or higher invoices';
83
CALL public.update_invoice_next_status_for_invoice_header(p_company_id, v_invoice_ids_other, p_modified_by);
84
END IF;
85
86
EXCEPTION WHEN OTHERS THEN
87
v_error_message := SQLERRM;
88
RAISE NOTICE 'Exception in child procedures: %', v_error_message;
89
END;
90
91
-- Return rows from temp_invoice_next_status
92
RAISE NOTICE 'Preparing to return rows from temp_invoice_next_status';
93
RETURN QUERY
94
SELECT tinv.invoice_id, tinv.status AS status_name, tinv.error AS error_msg
95
FROM temp_invoice_next_status tinv
96
WHERE tinv.invoice_id = ANY(p_invoice_ids); -- Directly filter by the input invoice IDs
97
98
END;
99
$function$
|
|||||
| Function | get_invoice_header_by_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_header_by_id(p_invoice_header_id uuid)
2
RETURNS TABLE(id uuid, invoice_number text, invoice_voucher text, customer_id uuid, customer_name text, due_date timestamp with time zone, invoice_date timestamp with time zone, payment_term integer, total_amount numeric, settled_amount numeric, discount numeric, note text, currency_id integer, invoice_status_id integer, invoice_status text, type integer, is_created_by_scheduler boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
-- Query for the specific InvoiceHeader
8
SELECT header.id,
9
header.invoice_number,
10
header.invoice_voucher,
11
customer.id AS customer_id,
12
customer.name::text AS customer_name, -- Cast to text
13
header.due_date::timestamptz,
14
header.invoice_date::timestamptz,
15
header.payment_term,
16
header.total_amount,
17
header.settled_amount,
18
header.discount,
19
header.note,
20
header.currency_id,
21
status.id AS invoice_status_id,
22
status.name::text AS invoice_status, -- Cast to text
23
header.type,
24
header.is_created_by_scheduler
25
FROM invoice_headers header
26
JOIN customers customer ON customer.id = header.customer_id
27
JOIN invoice_statuses status ON status.id = header.invoice_status_id
28
WHERE header.id = p_invoice_header_id
29
AND header.is_deleted = false;
30
END;
31
$function$
|
|||||
| Function | get_invoice_payments_by_customer_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_payments_by_customer_id(p_company_id uuid, p_customer_id uuid, p_from_date date, p_to_date date)
2
RETURNS TABLE(company_id uuid, customer_id uuid, customer_name character varying, id uuid, payment_number text, mode_of_payment text, reference text, received_date timestamp without time zone, received_amount numeric, grand_total_amount numeric, advance_amount numeric, tds_amount numeric, description text, debit_account_id uuid, credit_account_id uuid, transaction_id bigint, is_posted boolean, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, is_deleted boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
iph.company_id,
9
iph.customer_id,
10
c.name AS customer_name,
11
iph.id AS id,
12
iph.payment_number,
13
iph.mode_of_payment,
14
iph.reference,
15
iph.received_date,
16
iph.received_amount,
17
iph.grand_total_amount,
18
iph.advance_amount,
19
iph.tds_amount,
20
iph.description,
21
iph.debit_account_id,
22
iph.credit_account_id,
23
iph.transaction_id,
24
iph.is_posted,
25
iph.created_on_utc,
26
iph.modified_on_utc,
27
iph.is_deleted
28
FROM
29
invoice_payment_headers iph
30
INNER JOIN
31
customers c ON iph.customer_id = c.id
32
WHERE
33
iph.company_id = p_company_id
34
AND iph.customer_id = p_customer_id
35
AND iph.is_deleted = FALSE
36
AND c.is_deleted = FALSE
37
AND iph.received_date BETWEEN p_from_date AND p_to_date
38
ORDER BY
39
iph.received_date DESC;
40
END;
41
$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, p_from_date date, p_to_date date)
2
RETURNS TABLE(company_id uuid, customer_id uuid, customer_name character varying, id uuid, invoice_number text, invoice_voucher text, invoice_date timestamp without time zone, due_date timestamp without time zone, total_amount numeric, settled_amount numeric, due_amount numeric, payment_status_id integer, fin_entry_status_id integer, transaction_id bigint, is_posted boolean, current_approval_level integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, is_deleted boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
i.company_id,
9
i.customer_id,
10
c.name AS customer_name,
11
i.id AS id,
12
i.invoice_number,
13
i.invoice_voucher,
14
i.invoice_date,
15
i.due_date,
16
i.total_amount,
17
i.settled_amount,
18
(i.total_amount - COALESCE(i.settled_amount, 0))::numeric AS due_amount,
19
i.payment_status_id,
20
i.fin_entry_status_id,
21
i.transaction_id,
22
i.is_posted,
23
i.current_approval_level,
24
i.created_on_utc,
25
i.modified_on_utc,
26
i.is_deleted
27
FROM
28
invoice_headers i
29
INNER JOIN
30
customers c ON i.customer_id = c.id
31
WHERE
32
i.company_id = p_company_id
33
AND i.customer_id = p_customer_id
34
AND i.is_deleted = FALSE
35
AND c.is_deleted = FALSE
36
-- Unpaid or partially paid invoices
37
AND (i.settled_amount IS NULL OR i.settled_amount < i.total_amount)
38
-- Due date filter
39
AND i.due_date BETWEEN p_from_date AND p_to_date
40
ORDER BY
41
i.due_date ASC;
42
END;
43
$function$
|
|||||
| Function | get_customers_with_invoice_summary | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_customers_with_invoice_summary(p_company_id uuid, p_fin_year_id integer)
2
RETURNS TABLE(id uuid, name character varying, contact_name text, gst_in text, mobile_number text, email text, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, billing_address jsonb, contact_count integer, total_invoice_amount numeric, total_settled_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
6
DECLARE
7
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
8
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
9
BEGIN
10
RETURN QUERY
11
SELECT
12
c.id,
13
c.name,
14
con.first_name || ' ' || con.last_name AS contact_name,
15
c.gstin AS gst_in,
16
con.mobile_number,
17
con.email,
18
c.created_by,
19
u_created.first_name || ' ' || u_created.last_name AS created_by_name,
20
c.modified_by,
21
u_modified.first_name || ' ' || u_modified.last_name AS modified_by_name,
22
c.created_on_utc,
23
c.modified_on_utc,
24
CASE
25
WHEN ba.id IS NOT NULL THEN jsonb_build_object(
26
'AddressLine1', ba.address_line1,
27
'AddressLine2', ba.address_line2,
28
'ZipCode', ba.zip_code,
29
'CountryId', ba.country_id,
30
'StateId', ba.state_id,
31
'CityId', ba.city_id
32
)
33
ELSE NULL
34
END AS billing_address,
35
(SELECT COUNT(*)::integer FROM public.customer_contacts cc_count WHERE cc_count.customer_id = c.id) AS contact_count,
36
COALESCE(inv_sums.total_invoice_amount, 0) AS total_invoice_amount,
37
COALESCE(inv_sums.total_settled_amount, 0) AS total_settled_amount
38
FROM public.customers c
39
LEFT JOIN public.customer_contacts cc ON c.id = cc.customer_id
40
LEFT JOIN public.contacts con ON cc.contact_id = con.id AND con.is_primary = true
41
LEFT JOIN public.users u_created ON c.created_by = u_created.id
42
LEFT JOIN public.users u_modified ON c.modified_by = u_modified.id
43
LEFT JOIN public.addresses ba ON c.billing_address_id = ba.id
44
LEFT JOIN (
45
SELECT
46
ih.customer_id,
47
SUM(ih.total_amount) AS total_invoice_amount,
48
SUM(ih.settled_amount) AS total_settled_amount
49
FROM public.invoice_headers ih
50
WHERE ih.company_id = p_company_id
51
AND ih.is_deleted = false
52
AND ih.invoice_date >= v_start_date
53
AND ih.invoice_date <= v_end_date
54
GROUP BY ih.customer_id
55
) inv_sums ON inv_sums.customer_id = c.id
56
WHERE c.company_id = p_company_id
57
AND c.is_deleted = false;
58
END;
59
$function$
|
|||||
| Function | get_defaultors | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_defaultors(p_company_id uuid)
2
RETURNS TABLE(id uuid, customer_name text, invoice_number text, created_date timestamp without time zone, due_date timestamp without time zone, amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
-- Calculate and return the list of defaultors (overdue invoices)
7
RETURN QUERY
8
SELECT
9
gen_random_uuid() AS id, -- Generate a random unique ID
10
c.name::TEXT, -- Customer Name
11
i.invoice_number, -- Invoice Number
12
i.created_on_utc AS created_date, -- Created Date
13
i.due_date, -- Due Date
14
i.total_amount AS amount -- Total Invoice Amount
15
FROM
16
invoice_headers i
17
JOIN
18
customers c ON i.customer_id = c.id
19
JOIN
20
company_defaultor_configurations cfg ON i.company_id = cfg.company_id
21
WHERE
22
i.company_id = p_company_id
23
AND i.due_date < CURRENT_DATE - INTERVAL '1 day' * cfg.due_period_days -- Overdue invoices based on company configuration
24
AND i.settled_amount < i.total_amount -- Only unpaid invoices
25
AND i.is_deleted = FALSE -- Exclude deleted invoices
26
AND c.is_deleted = FALSE; -- Exclude deleted customers
27
END;
28
$function$
|
|||||
| Function | get_execution_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_execution_invoices(p_execution_id uuid)
2
RETURNS TABLE(invoice_header_id uuid, invoice_number text, invoice_date timestamp without time zone, payment_term numeric, due_date timestamp without time zone, total_amount numeric, settled_amount numeric, taxable_amount numeric, discount numeric, sgst_amount numeric, cgst_amount numeric, igst_amount numeric, round_off numeric, note text, customer_id uuid, customer_name text, invoice_status_id integer, invoice_status text, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, credit_account_id uuid, invoice_voucher text, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name text, type integer, is_created_by_scheduler boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
ih.id AS invoice_header_id,
9
ih.invoice_number::text AS invoice_number, -- Explicit cast
10
ih.invoice_date,
11
ih.payment_term::numeric AS payment_term, -- Cast to numeric
12
ih.due_date,
13
ih.total_amount,
14
ih.settled_amount,
15
ih.taxable_amount,
16
ih.discount,
17
ih.sgst_amount,
18
ih.cgst_amount,
19
ih.igst_amount,
20
ih.round_off,
21
ih.note::text AS note, -- Explicit cast
22
ih.customer_id,
23
COALESCE(c.name, 'Unknown')::text AS customer_name, -- Explicit cast
24
ih.invoice_status_id,
25
COALESCE(s.name, 'Unknown')::text AS invoice_status, -- Explicit cast
26
ih.currency_id,
27
ih.created_on_utc,
28
ih.modified_on_utc,
29
ih.credit_account_id,
30
ih.invoice_voucher::text AS invoice_voucher, -- Explicit cast
31
ih.created_by,
32
ih.modified_by,
33
ih.source_warehouse_id,
34
ih.destination_warehouse_id,
35
COALESCE(w.name, 'Unknown')::text AS destination_warehouse_name, -- Explicit cast
36
ih.type,
37
ih.is_created_by_scheduler
38
FROM
39
apartment_invoice_postings aip
40
INNER JOIN
41
invoice_headers ih ON aip.invoice_header_id = ih.id
42
LEFT JOIN
43
customers c ON ih.customer_id = c.id
44
LEFT JOIN
45
invoice_statuses s ON ih.invoice_status_id = s.id
46
LEFT JOIN
47
warehouses w ON ih.destination_warehouse_id = w.id
48
WHERE
49
aip.execution_id = p_execution_id
50
AND NOT aip.is_deleted
51
AND NOT ih.is_deleted;
52
END;
53
$function$
|
|||||
| Function | get_total_defaultors_and_amount | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_total_defaultors_and_amount(p_company_id uuid)
2
RETURNS TABLE(result_id uuid, total_defaultors integer, total_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
-- Generate a unique id for this result
7
result_id := gen_random_uuid();
8
9
-- Debugging: Output the company_id being passed
10
RAISE NOTICE 'Calculating for company_id: %', p_company_id;
11
12
-- Calculate total defaultors and total amount for overdue invoices
13
RETURN QUERY
14
SELECT
15
result_id As id, -- Return the generated unique id for each result
16
COUNT(DISTINCT i.customer_id)::INT AS total_defaultors, -- Cast COUNT to INT
17
SUM(i.total_amount) AS total_amount -- Sum of total amounts of overdue invoices
18
FROM
19
invoice_headers i
20
JOIN
21
customers c ON i.customer_id = c.id -- Join with customers to filter by company_id
22
WHERE
23
i.company_id = p_company_id -- Use the passed-in parameter (p_company_id)
24
AND i.due_date < CURRENT_DATE - (SELECT due_period_days FROM company_defaultor_configurations WHERE company_id = p_company_id LIMIT 1) -- Check for overdue invoices based on due_period_days
25
AND i.settled_amount < i.total_amount -- Only unpaid invoices (settled_amount < total_amount)
26
AND i.is_deleted = FALSE -- Exclude deleted invoices
27
AND c.is_deleted = FALSE; -- Exclude deleted customers
28
END;
29
$function$
|
|||||
| Function | get_grouped_invoice_details | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_details(p_group_invoice_header_id uuid, p_fin_year integer)
2
RETURNS TABLE(invoice_header_id uuid, invoice_number text, invoice_date timestamp without time zone, payment_term numeric, due_date timestamp without time zone, total_amount numeric, settled_amount numeric, taxable_amount numeric, discount numeric, sgst_amount numeric, cgst_amount numeric, igst_amount numeric, round_off numeric, note text, customer_id uuid, customer_name text, invoice_status_id integer, invoice_status text, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, credit_account_id uuid, invoice_voucher text, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name text, type integer, is_created_by_scheduler boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
6
DECLARE
7
v_start_date date := MAKE_DATE(p_fin_year, 4, 1);
8
v_end_date date := MAKE_DATE(p_fin_year + 1, 3, 31);
9
BEGIN
10
RETURN QUERY
11
SELECT
12
ih.id AS invoice_header_id,
13
ih.invoice_number::text AS invoice_number,
14
ih.invoice_date,
15
ih.payment_term::numeric AS payment_term,
16
ih.due_date,
17
ih.total_amount,
18
ih.settled_amount,
19
ih.taxable_amount,
20
ih.discount,
21
ih.sgst_amount,
22
ih.cgst_amount,
23
ih.igst_amount,
24
ih.round_off,
25
ih.note::text AS note,
26
ih.customer_id,
27
COALESCE(c.name, 'Unknown')::text AS customer_name,
28
ih.invoice_status_id,
29
COALESCE(s.name, 'Unknown')::text AS invoice_status,
30
ih.currency_id,
31
ih.created_on_utc,
32
ih.modified_on_utc,
33
ih.credit_account_id,
34
ih.invoice_voucher::text AS invoice_voucher,
35
ih.created_by,
36
ih.modified_by,
37
ih.source_warehouse_id,
38
ih.destination_warehouse_id,
39
COALESCE(w.name, 'Unknown')::text AS destination_warehouse_name,
40
ih.type,
41
ih.is_created_by_scheduler
42
FROM
43
public.group_invoice_details aid
44
INNER JOIN
45
invoice_headers ih ON aid.invoice_header_id = ih.id
46
LEFT JOIN
47
customers c ON ih.customer_id = c.id
48
LEFT JOIN
49
invoice_statuses s ON ih.invoice_status_id = s.id
50
LEFT JOIN
51
warehouses w ON ih.destination_warehouse_id = w.id
52
WHERE
53
aid.group_invoice_header_id = p_group_invoice_header_id
54
AND NOT aid.is_deleted
55
AND NOT ih.is_deleted
56
AND (p_fin_year IS NULL OR (ih.invoice_date >= v_start_date AND ih.invoice_date <= v_end_date));
57
END;
58
$function$
|
|||||
| Function | get_group_invoice_totals | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_group_invoice_totals(p_company_id uuid, p_grouped_invoice_id uuid)
2
RETURNS TABLE(id uuid, grouped_invoice_name text, total_amount numeric, paid_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
6
BEGIN
7
RETURN QUERY
8
SELECT
9
gih.id AS id,
10
gih.group_invoice_number AS grouped_invoice_name,
11
SUM(ih.total_amount) AS total_amount,
12
SUM(
13
CASE
14
WHEN ih.invoice_status_id = 3 THEN ih.settled_amount
15
ELSE 0
16
END
17
) AS paid_amount
18
FROM
19
public.group_invoice_headers gih
20
JOIN
21
public.group_invoice_details gid ON gih.id = gid.group_invoice_header_id
22
JOIN
23
public.invoice_headers ih ON gid.invoice_header_id = ih.id
24
WHERE
25
gih.id = p_grouped_invoice_id
26
AND gih.company_id = p_company_id
27
AND gih.is_deleted = false
28
GROUP BY
29
gih.id,
30
gih.group_invoice_number;
31
END;
32
$function$
|
|||||
| Function | get_invoice_analytics | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_analytics(p_company_id uuid, p_finance_year_id integer, p_group_invoice_header_id uuid DEFAULT NULL::uuid)
2
RETURNS TABLE(id integer, status text, count integer, total_amount numeric, month_start_date date)
3
LANGUAGE plpgsql
4
AS $function$
5
6
DECLARE
7
v_financial_year_start DATE;
8
v_financial_year_end DATE;
9
BEGIN
10
v_financial_year_start := TO_DATE(p_finance_year_id || '-04-01', 'YYYY-MM-DD');
11
v_financial_year_end := TO_DATE((p_finance_year_id + 1) || '-03-31', 'YYYY-MM-DD');
12
13
-- Case 1: Group Invoice Specific
14
IF p_group_invoice_header_id IS NOT NULL THEN
15
IF NOT EXISTS (
16
SELECT 1
17
FROM group_invoice_headers gih
18
WHERE gih.id = p_group_invoice_header_id
19
AND gih.is_deleted = false
20
) THEN
21
RETURN;
22
END IF;
23
24
RETURN QUERY
25
SELECT row_number() OVER ()::int AS id,
26
'Paid' AS status,
27
COUNT(*)::int,
28
COALESCE(SUM(ih.settled_amount),0),
29
DATE_TRUNC('month', ih.invoice_date)::date
30
FROM group_invoice_details gid
31
JOIN invoice_headers ih ON ih.id = gid.invoice_header_id
32
WHERE gid.group_invoice_header_id = p_group_invoice_header_id
33
AND gid.is_deleted = false
34
AND ih.invoice_status_id IN (4,5)
35
AND ih.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
36
GROUP BY DATE_TRUNC('month', ih.invoice_date)
37
38
UNION ALL
39
SELECT row_number() OVER ()::int,
40
'Pending',
41
COUNT(*)::int,
42
COALESCE(SUM(ih.total_amount - ih.settled_amount),0),
43
DATE_TRUNC('month', ih.invoice_date)::date
44
FROM group_invoice_details gid
45
JOIN invoice_headers ih ON ih.id = gid.invoice_header_id
46
WHERE gid.group_invoice_header_id = p_group_invoice_header_id
47
AND gid.is_deleted = false
48
AND ih.invoice_status_id IN (3,4)
49
AND ih.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
50
GROUP BY DATE_TRUNC('month', ih.invoice_date)
51
52
UNION ALL
53
SELECT row_number() OVER ()::int,
54
'Overdue',
55
COUNT(*)::int,
56
COALESCE(SUM(ih.total_amount - ih.settled_amount),0),
57
DATE_TRUNC('month', ih.invoice_date)::date
58
FROM group_invoice_details gid
59
JOIN invoice_headers ih ON ih.id = gid.invoice_header_id
60
WHERE gid.group_invoice_header_id = p_group_invoice_header_id
61
AND gid.is_deleted = false
62
AND ih.invoice_status_id IN (3,4)
63
AND ih.due_date < now()
64
AND ih.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
65
GROUP BY DATE_TRUNC('month', ih.invoice_date);
66
67
-- Case 2: Company-wide Analytics
68
ELSE
69
RETURN QUERY
70
SELECT row_number() OVER ()::int,
71
'Paid',
72
COUNT(*)::int,
73
COALESCE(SUM(i.settled_amount), 0),
74
DATE_TRUNC('month', i.invoice_date)::date
75
FROM invoice_headers i
76
WHERE i.company_id = p_company_id
77
AND i.invoice_status_id IN (4, 5)
78
AND i.is_deleted = false
79
AND i.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
80
GROUP BY DATE_TRUNC('month', i.invoice_date)
81
82
UNION ALL
83
SELECT row_number() OVER ()::int,
84
'Pending',
85
COUNT(*)::int,
86
COALESCE(SUM(i.total_amount - i.settled_amount), 0),
87
DATE_TRUNC('month', i.invoice_date)::date
88
FROM invoice_headers i
89
WHERE i.company_id = p_company_id
90
AND i.invoice_status_id IN (3, 4)
91
AND i.is_deleted = false
92
AND i.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
93
GROUP BY DATE_TRUNC('month', i.invoice_date)
94
95
UNION ALL
96
SELECT row_number() OVER ()::int,
97
'Overdue',
98
COUNT(*)::int,
99
COALESCE(SUM(i.total_amount - i.settled_amount), 0),
100
DATE_TRUNC('month', i.invoice_date)::date
101
FROM invoice_headers i
102
WHERE i.company_id = p_company_id
103
AND i.invoice_status_id IN (3, 4)
104
AND i.due_date < CURRENT_DATE
105
AND i.is_deleted = false
106
AND i.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
107
GROUP BY DATE_TRUNC('month', i.invoice_date);
108
END IF;
109
END;
110
$function$
|
|||||
| Function | get_grouped_invoice_header_by_limit | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_header_by_limit(p_company_id uuid, p_fin_year_id integer, p_limit integer DEFAULT NULL::integer)
2
RETURNS TABLE(id uuid, group_invoice_number text, grouped_invoice_template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text, group_invoice_batch_id text, paid_count integer, total_paid_amount numeric, original_total_amount numeric, remaining_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
7
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
8
BEGIN
9
IF p_limit IS NOT NULL THEN
10
-- If p_limit is provided, apply the LIMIT
11
RETURN QUERY
12
SELECT
13
gih.id,
14
gih.group_invoice_number,
15
gih.group_invoice_template_id,
16
gih.execution_date,
17
gih.error_message,
18
gih.success_count,
19
gih.total_count,
20
templates.invoice_description,
21
gih.group_invoice_batch_id,
22
CAST(COALESCE(SUM(CASE WHEN ih.settled_amount >= ih.total_amount THEN 1 ELSE 0 END), 0) AS integer) AS paid_count,
23
COALESCE(SUM(ih.settled_amount), 0) AS total_paid_amount,
24
COALESCE(SUM(ih.total_amount), 0) AS original_total_amount,
25
COALESCE(SUM(ih.total_amount - ih.settled_amount), 0) AS remaining_amount
26
FROM
27
public.group_invoice_headers gih
28
INNER JOIN
29
public.group_invoice_templates templates
30
ON gih.group_invoice_template_id = templates.id
31
LEFT JOIN
32
public.group_invoice_details gid
33
ON gid.group_invoice_header_id = gih.id
34
AND gid.is_deleted = false
35
AND gid.company_id = p_company_id
36
LEFT JOIN
37
public.invoice_headers ih
38
ON ih.id = gid.invoice_header_id
39
AND ih.is_deleted = false
40
AND ih.company_id = p_company_id
41
WHERE
42
gih.company_id = p_company_id
43
AND gih.execution_date::date BETWEEN v_start_date AND v_end_date
44
GROUP BY
45
gih.id,
46
gih.group_invoice_number,
47
gih.group_invoice_template_id,
48
gih.execution_date,
49
gih.error_message,
50
gih.success_count,
51
gih.total_count,
52
templates.invoice_description,
53
gih.group_invoice_batch_id
54
ORDER BY
55
gih.execution_date DESC
56
LIMIT p_limit; -- Apply the LIMIT when p_limit is provided
57
ELSE
58
-- If p_limit is NULL, return all results for the specified company and financial year
59
RETURN QUERY
60
SELECT
61
gih.id,
62
gih.group_invoice_number,
63
gih.group_invoice_template_id,
64
gih.execution_date,
65
gih.error_message,
66
gih.success_count,
67
gih.total_count,
68
templates.invoice_description,
69
gih.group_invoice_batch_id,
70
CAST(COALESCE(SUM(CASE WHEN ih.settled_amount >= ih.total_amount THEN 1 ELSE 0 END), 0) AS integer) AS paid_count,
71
COALESCE(SUM(ih.settled_amount), 0) AS total_paid_amount,
72
COALESCE(SUM(ih.total_amount), 0) AS original_total_amount,
73
COALESCE(SUM(ih.total_amount - ih.settled_amount), 0) AS remaining_amount
74
FROM
75
public.group_invoice_headers gih
76
INNER JOIN
77
public.group_invoice_templates templates
78
ON gih.group_invoice_template_id = templates.id
79
LEFT JOIN
80
public.group_invoice_details gid
81
ON gid.group_invoice_header_id = gih.id
82
AND gid.is_deleted = false
83
AND gid.company_id = p_company_id
84
LEFT JOIN
85
public.invoice_headers ih
86
ON ih.id = gid.invoice_header_id
87
AND ih.is_deleted = false
88
AND ih.company_id = p_company_id
89
WHERE
90
gih.company_id = p_company_id
91
AND gih.execution_date::date BETWEEN v_start_date AND v_end_date
92
GROUP BY
93
gih.id,
94
gih.group_invoice_number,
95
gih.group_invoice_template_id,
96
gih.execution_date,
97
gih.error_message,
98
gih.success_count,
99
gih.total_count,
100
templates.invoice_description,
101
gih.group_invoice_batch_id
102
ORDER BY
103
gih.execution_date DESC;
104
END IF;
105
END;
106
$function$
|
|||||
| Function | get_all_invoices_from_span | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_invoices_from_span(p_company_id uuid, p_fin_year_id integer, p_user_id uuid, p_type_id integer DEFAULT NULL::integer, p_start_date timestamp without time zone DEFAULT NULL::timestamp without time zone, p_end_date timestamp without time zone DEFAULT NULL::timestamp without time zone)
2
RETURNS TABLE(id uuid, invoice_number character varying, type integer, invoice_date date, customer_id uuid, customer_name character varying, due_date date, total_amount numeric, settled_amount numeric, invoice_status_id integer, invoice_status character varying, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, created_by uuid, modified_by uuid, created_by_name character varying, modified_by_name character varying, is_created_by_scheduler boolean, has_next_status_approval boolean, next_status_id integer, has_pending_payment_verification boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date := COALESCE(p_start_date::date, MAKE_DATE(p_fin_year_id, 4, 1));
7
v_end_date date := COALESCE(p_end_date::date, MAKE_DATE(p_fin_year_id + 1, 3, 31));
8
DRAFT_STATUS CONSTANT integer := 1;
9
v_organization_id uuid;
10
BEGIN
11
RETURN QUERY
12
WITH invoice_data AS (
13
SELECT
14
ih.id,
15
CAST(ih.invoice_number AS varchar) AS invoice_number,
16
ih.type,
17
ih.invoice_date::date,
18
ih.customer_id,
19
c.name AS customer_name,
20
ih.due_date::date,
21
ih.total_amount,
22
ih.settled_amount,
23
ih.invoice_status_id,
24
s.name AS invoice_status,
25
ih.currency_id,
26
ih.created_on_utc,
27
ih.modified_on_utc,
28
ih.created_by,
29
ih.modified_by,
30
CAST(CONCAT(cu.first_name, ' ', cu.last_name) AS varchar) AS created_by_name,
31
CAST(CONCAT(mu.first_name, ' ', mu.last_name) AS varchar) AS modified_by_name,
32
ih.is_created_by_scheduler,
33
ih.credit_account_id
34
FROM invoice_headers ih
35
LEFT JOIN customers c ON c.id = ih.customer_id
36
LEFT JOIN invoice_statuses s ON s.id = ih.invoice_status_id
37
LEFT JOIN users cu ON cu.id = ih.created_by
38
LEFT JOIN users mu ON mu.id = ih.modified_by
39
WHERE ih.company_id = p_company_id
40
AND ih.is_deleted = false
41
AND ih.invoice_date BETWEEN v_start_date AND v_end_date
42
AND (p_type_id IS NULL OR p_type_id = 0 OR ih.type = p_type_id)
43
UNION ALL
44
SELECT
45
dih.id,
46
CAST(dih.invoice_number AS varchar) AS invoice_number,
47
dih.type,
48
dih.invoice_date::date,
49
dih.customer_id,
50
c.name AS customer_name,
51
dih.due_date::date,
52
dih.total_amount,
53
dih.settled_amount,
54
dih.invoice_status_id,
55
s.name AS invoice_status,
56
dih.currency_id,
57
dih.created_on_utc,
58
dih.modified_on_utc,
59
dih.created_by,
60
dih.modified_by,
61
CAST(CONCAT(cu.first_name, ' ', cu.last_name) AS varchar) AS created_by_name,
62
CAST(CONCAT(mu.first_name, ' ', mu.last_name) AS varchar) AS modified_by_name,
63
dih.is_created_by_scheduler,
64
dih.credit_account_id
65
FROM draft_invoice_headers dih
66
LEFT JOIN customers c ON c.id = dih.customer_id
67
LEFT JOIN invoice_statuses s ON s.id = dih.invoice_status_id
68
LEFT JOIN users cu ON cu.id = dih.created_by
69
LEFT JOIN users mu ON mu.id = dih.modified_by
70
WHERE dih.company_id = p_company_id
71
AND dih.is_deleted = false
72
AND dih.invoice_date BETWEEN v_start_date AND v_end_date
73
AND (p_type_id IS NULL OR p_type_id = 0 OR dih.type = p_type_id)
74
)
75
SELECT
76
id.id,
77
id.invoice_number,
78
id.type,
79
id.invoice_date,
80
id.customer_id,
81
id.customer_name,
82
id.due_date,
83
id.total_amount,
84
id.settled_amount,
85
id.invoice_status_id,
86
id.invoice_status,
87
id.currency_id,
88
id.created_on_utc,
89
id.modified_on_utc,
90
id.created_by,
91
id.modified_by,
92
id.created_by_name,
93
id.modified_by_name,
94
id.is_created_by_scheduler,
95
-- Determine if current user can approve
96
CASE
97
WHEN id.invoice_status_id = DRAFT_STATUS THEN true
98
WHEN EXISTS (
99
SELECT 1
100
FROM public.invoice_approval_users_account a
101
WHERE a.account_id = id.credit_account_id
102
AND a.status_id = id.invoice_status_id
103
AND a.user_id = p_user_id
104
AND a.is_deleted = false
105
) THEN true
106
WHEN EXISTS (
107
SELECT 1
108
FROM public.invoice_approval_user_company c
109
WHERE c.company_id = p_company_id
110
AND c.status_id = id.invoice_status_id
111
AND c.user_id = p_user_id
112
AND c.is_deleted = false
113
) THEN true
114
ELSE false
115
END AS has_next_status_approval,
116
-- Determine next status
117
COALESCE(
118
(SELECT iw.next_status
119
FROM invoice_workflow iw
120
WHERE iw.company_id = p_company_id
121
AND iw.status = id.invoice_status_id
122
AND iw.is_deleted = false
123
LIMIT 1),
124
0
125
) AS next_status_id,
126
-- Check if there's a pending payment verification
127
EXISTS (
128
SELECT 1
129
FROM public.invoice_payment_headers iph
130
JOIN public.invoice_payment_details ipd
131
ON ipd.invoice_payment_header_id = iph.id
132
WHERE ipd.invoice_header_id = id.id
133
AND iph.payment_status_id = 4 -- Payment Pending Verification status
134
AND iph.is_deleted = false
135
) AS has_pending_payment_verification
136
FROM invoice_data id;
137
END;
138
$function$
|
|||||
| Function | get_unit_dues_by_cust_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_unit_dues_by_cust_id(p_company_id uuid, p_customer_id uuid, p_fin_year_id integer)
2
RETURNS TABLE(id uuid, invoice_number text, note text, due_date timestamp without time zone, total_due numeric, total_paid numeric, original_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1); -- Default: April 1st of the given financial year
7
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31); -- Default: March 31st of the next financial year
8
BEGIN
9
RETURN QUERY
10
SELECT
11
i.id AS id,
12
i.invoice_number,
13
i.note,
14
i.due_date,
15
(i.total_amount - i.settled_amount) AS total_due,
16
i.settled_amount AS total_paid,
17
i.total_amount AS original_amount
18
FROM invoice_headers i
19
INNER JOIN customers c
20
ON c.id = i.customer_id
21
AND c.company_id = p_company_id
22
WHERE
23
i.company_id = p_company_id
24
AND i.customer_id = p_customer_id
25
AND c.is_deleted = false
26
AND i.is_deleted = false
27
AND i.invoice_status_id IN (3, 4) -- Approved / Partially Paid
28
AND (i.total_amount - i.settled_amount) > 0
29
AND i.due_date >= v_start_date -- Filter by the financial year start date
30
AND i.due_date <= v_end_date -- Filter by the financial year end date
31
ORDER BY i.due_date;
32
END;
33
$function$
|
|||||
| Function | get_defaulter_list | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_defaulter_list(p_company_id uuid, p_as_of_date timestamp without time zone, p_limit integer DEFAULT NULL::integer)
2
RETURNS TABLE(id integer, customer_id uuid, customer_name character varying, due_date date, days_overdue integer, total_outstanding_per_customer numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT *
8
FROM (
9
SELECT
10
ROW_NUMBER() OVER (
11
ORDER BY
12
SUM(ih.total_amount - ih.settled_amount) DESC,
13
MAX(DATE_PART('day', p_as_of_date - ih.due_date)) DESC,
14
c."name" ASC
15
)::INT AS id,
16
17
c.id AS customer_id,
18
c."name" AS customer_name,
19
20
-- Oldest unpaid invoice due date
21
MIN(ih.due_date)::DATE AS due_date,
22
23
-- Max overdue days across invoices
24
MAX(
25
DATE_PART('day', p_as_of_date - ih.due_date)
26
)::INT AS days_overdue,
27
28
SUM(ih.total_amount - ih.settled_amount)
29
AS total_outstanding_per_customer
30
31
FROM public.invoice_headers ih
32
JOIN public.customers c
33
ON c.id = ih.customer_id
34
LEFT JOIN public.defaulter_configurations dc
35
ON dc.company_id = p_company_id
36
37
WHERE
38
ih.settled_amount < ih.total_amount
39
AND ih.due_date < p_as_of_date
40
AND ih.is_deleted = FALSE
41
AND c.is_deleted = FALSE
42
AND ih.company_id = p_company_id
43
AND dc.is_deleted = FALSE
44
AND DATE_PART('day', p_as_of_date - ih.due_date)
45
> dc.defaulter_period_days
46
47
GROUP BY
48
c.id,
49
c."name"
50
) ranked
51
ORDER BY
52
ranked.id
53
LIMIT
54
COALESCE(p_limit, 2147483647);
55
END;
56
$function$
|
|||||
| Function | get_unit_dues_summary_by_cust_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_unit_dues_summary_by_cust_id(p_company_id uuid, p_customer_id uuid, p_fin_year_id integer)
2
RETURNS TABLE(last_collection_target_value numeric, last_collection_collected_value numeric, last_collection_units_targeted integer, last_collection_paid_units integer, total_outstanding_due numeric, total_units_not_paid integer)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
7
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
8
9
v_latest_cycle_date date;
10
BEGIN
11
/*
12
* 1) Find latest invoice cycle date within the FY
13
* Using invoice_date if present, otherwise created_on_utc
14
*/
15
SELECT MAX(COALESCE(i.invoice_date::date, i.created_on_utc::date))
16
INTO v_latest_cycle_date
17
FROM invoice_headers i
18
INNER JOIN customers c ON c.id = i.customer_id
19
WHERE i.company_id = p_company_id
20
AND i.customer_id = p_customer_id
21
AND c.company_id = p_company_id
22
AND c.is_deleted = false
23
AND i.is_deleted = false
24
AND i.invoice_status_id IN (3, 4) -- Approved / Partially Paid
25
AND i.due_date::date BETWEEN v_start_date AND v_end_date;
26
27
-- No invoices in FY → return zero summary
28
IF v_latest_cycle_date IS NULL THEN
29
RETURN QUERY
30
SELECT 0, 0, 0, 0, 0, 0;
31
RETURN;
32
END IF;
33
34
/*
35
* 2) Build FY invoice set
36
*/
37
RETURN QUERY
38
WITH fy_invoices AS (
39
SELECT
40
i.customer_id,
41
i.total_amount,
42
i.settled_amount,
43
(i.total_amount - i.settled_amount) AS due_amount,
44
COALESCE(i.invoice_date::date, i.created_on_utc::date) AS cycle_date
45
FROM invoice_headers i
46
INNER JOIN customers c ON c.id = i.customer_id
47
WHERE i.company_id = p_company_id
48
AND i.customer_id = p_customer_id
49
AND c.company_id = p_company_id
50
AND c.is_deleted = false
51
AND i.is_deleted = false
52
AND i.invoice_status_id IN (3, 4)
53
AND i.due_date::date BETWEEN v_start_date AND v_end_date
54
),
55
last_cycle AS (
56
SELECT *
57
FROM fy_invoices
58
WHERE cycle_date = v_latest_cycle_date
59
),
60
last_cycle_rollup AS (
61
SELECT
62
COALESCE(SUM(total_amount), 0) AS target_value,
63
COALESCE(SUM(settled_amount), 0) AS collected_value,
64
COUNT(DISTINCT customer_id)::int AS units_targeted,
65
COUNT(
66
DISTINCT CASE
67
WHEN due_amount <= 0 THEN customer_id
68
END
69
)::int AS paid_units
70
FROM last_cycle
71
),
72
overall_rollup AS (
73
SELECT
74
COALESCE(SUM(due_amount), 0) AS total_due,
75
COUNT(
76
DISTINCT CASE
77
WHEN due_amount > 0 THEN customer_id
78
END
79
)::int AS units_not_paid
80
FROM fy_invoices
81
)
82
SELECT
83
l.target_value,
84
l.collected_value,
85
l.units_targeted,
86
l.paid_units,
87
o.total_due,
88
o.units_not_paid
89
FROM last_cycle_rollup l
90
CROSS JOIN overall_rollup o;
91
END;
92
$function$
|
|||||
| Function | get_unit_dues_summary_by_company | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_unit_dues_summary_by_company(p_company_id uuid, p_fin_year_id integer)
2
RETURNS TABLE(last_collection_target_value numeric, last_collection_collected_value numeric, last_collection_units_targeted integer, last_collection_paid_units integer, total_outstanding_due numeric, total_units_not_paid integer)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
7
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
8
9
v_latest_cycle_date date;
10
BEGIN
11
/*
12
* 1) Find latest invoice cycle date in FY (company-wide)
13
*/
14
SELECT MAX(COALESCE(i.invoice_date::date, i.created_on_utc::date))
15
INTO v_latest_cycle_date
16
FROM invoice_headers i
17
INNER JOIN customers c ON c.id = i.customer_id
18
WHERE i.company_id = p_company_id
19
AND c.company_id = p_company_id
20
AND c.is_deleted = false
21
AND i.is_deleted = false
22
AND i.invoice_status_id IN (3, 4) -- Approved / Partially Paid
23
AND i.due_date::date BETWEEN v_start_date AND v_end_date;
24
25
-- No invoices in FY → return zero summary
26
IF v_latest_cycle_date IS NULL THEN
27
RETURN QUERY
28
SELECT 0, 0, 0, 0, 0, 0;
29
RETURN;
30
END IF;
31
32
/*
33
* 2) Build FY invoice dataset
34
*/
35
RETURN QUERY
36
WITH fy_invoices AS (
37
SELECT
38
i.customer_id,
39
i.total_amount,
40
i.settled_amount,
41
(i.total_amount - i.settled_amount) AS due_amount,
42
COALESCE(i.invoice_date::date, i.created_on_utc::date) AS cycle_date
43
FROM invoice_headers i
44
INNER JOIN customers c ON c.id = i.customer_id
45
WHERE i.company_id = p_company_id
46
AND c.company_id = p_company_id
47
AND c.is_deleted = false
48
AND i.is_deleted = false
49
AND i.invoice_status_id IN (3, 4)
50
AND i.due_date::date BETWEEN v_start_date AND v_end_date
51
),
52
last_cycle AS (
53
SELECT *
54
FROM fy_invoices
55
WHERE cycle_date = v_latest_cycle_date
56
),
57
last_cycle_rollup AS (
58
SELECT
59
COALESCE(SUM(total_amount), 0) AS target_value,
60
COALESCE(SUM(settled_amount), 0) AS collected_value,
61
COUNT(DISTINCT customer_id)::int AS units_targeted,
62
COUNT(
63
DISTINCT CASE
64
WHEN due_amount <= 0 THEN customer_id
65
END
66
)::int AS paid_units
67
FROM last_cycle
68
),
69
overall_rollup AS (
70
SELECT
71
COALESCE(SUM(due_amount), 0) AS total_due,
72
COUNT(
73
DISTINCT CASE
74
WHEN due_amount > 0 THEN customer_id
75
END
76
)::int AS units_not_paid
77
FROM fy_invoices
78
)
79
SELECT
80
l.target_value,
81
l.collected_value,
82
l.units_targeted,
83
l.paid_units,
84
o.total_due,
85
o.units_not_paid
86
FROM last_cycle_rollup l
87
CROSS JOIN overall_rollup o;
88
END;
89
$function$
|
|||||
| Function | get_discussion_messages | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_discussion_messages(p_organization_id uuid, p_cycle_id uuid)
2
RETURNS jsonb
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_result jsonb;
7
BEGIN
8
-- Verify the cycle exists and belongs to the organization
9
IF NOT EXISTS (
10
SELECT 1
11
FROM public.delinquency_cycles dc
12
WHERE dc.id = p_cycle_id
13
AND dc.organization_id = p_organization_id
14
AND dc.is_deleted = false
15
) THEN
16
RETURN jsonb_build_object('messages', jsonb_build_array());
17
END IF;
18
19
-- Fetch all messages for the cycle
20
SELECT jsonb_build_object(
21
'messages',
22
COALESCE(
23
jsonb_agg(row_to_json(msg)),
24
jsonb_build_array()
25
)
26
)
27
INTO v_result
28
FROM (
29
SELECT
30
ddm.id,
31
ddm.cycle_id,
32
ddm.sender_id,
33
concat(u.first_name, ' ', u.last_name) AS sender_name,
34
ddm.sender_type,
35
ddm.message_text,
36
ddm.sent_on_utc
37
FROM public.delinquency_discussion_messages ddm
38
LEFT JOIN public.users u
39
ON u.id = ddm.sender_id
40
WHERE ddm.cycle_id = p_cycle_id
41
AND ddm.organization_id = p_organization_id
42
AND ddm.is_deleted = false
43
ORDER BY ddm.sent_on_utc ASC
44
) msg;
45
46
RETURN v_result;
47
END;
48
$function$
|
|||||
| Function | dblink_connect | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_connect(text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_connect$function$
|
|||||
| Function | dblink_connect | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_connect(text, text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_connect$function$
|
|||||
| Function | dblink_connect_u | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_connect_u(text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT SECURITY DEFINER
5
AS '$libdir/dblink', $function$dblink_connect$function$
|
|||||
| Function | dblink_connect_u | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_connect_u(text, text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT SECURITY DEFINER
5
AS '$libdir/dblink', $function$dblink_connect$function$
|
|||||
| Function | dblink_disconnect | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_disconnect()
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_disconnect$function$
|
|||||
| Function | dblink_disconnect | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_disconnect(text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_disconnect$function$
|
|||||
| Function | dblink_open | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_open(text, text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_open$function$
|
|||||
| Function | dblink_open | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_open(text, text, boolean)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_open$function$
|
|||||
| Function | dblink_open | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_open(text, text, text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_open$function$
|
|||||
| Function | dblink_open | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_open(text, text, text, boolean)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_open$function$
|
|||||
| Function | dblink_fetch | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_fetch(text, integer)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_fetch$function$
|
|||||
| Function | dblink_fetch | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_fetch(text, integer, boolean)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_fetch$function$
|
|||||
| Function | dblink_fetch | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_fetch(text, text, integer)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_fetch$function$
|
|||||
| Function | dblink_fetch | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_fetch(text, text, integer, boolean)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_fetch$function$
|
|||||
| Function | dblink_close | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_close(text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_close$function$
|
|||||
| Function | dblink_close | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_close(text, boolean)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_close$function$
|
|||||
| Function | dblink_close | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_close(text, text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_close$function$
|
|||||
| Function | dblink_close | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_close(text, text, boolean)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_close$function$
|
|||||
| Function | dblink | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink(text, text)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_record$function$
|
|||||
| Function | dblink | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink(text, text, boolean)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_record$function$
|
|||||
| Function | dblink | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink(text)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_record$function$
|
|||||
| Function | dblink | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink(text, boolean)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_record$function$
|
|||||
| Function | dblink_exec | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_exec(text, text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_exec$function$
|
|||||
| Function | dblink_exec | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_exec(text, text, boolean)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_exec$function$
|
|||||
| Function | dblink_exec | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_exec(text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_exec$function$
|
|||||
| Function | open_or_get_delinquency_cycle | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.open_or_get_delinquency_cycle(p_organization_id uuid, p_customer_id uuid, p_start_due_date date, p_created_by uuid)
2
RETURNS uuid
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_cycle_id uuid;
7
CYCLE_STATUS_OPEN CONSTANT int := 1;
8
BEGIN
9
-- Try to get existing open cycle
10
SELECT id
11
INTO v_cycle_id
12
FROM delinquency_cycles
13
WHERE organization_id = p_organization_id
14
AND customer_id = p_customer_id
15
AND ended_on_utc IS NULL
16
AND is_deleted = false
17
LIMIT 1;
18
19
IF v_cycle_id IS NOT NULL THEN
20
RETURN v_cycle_id;
21
END IF;
22
23
-- if not then Create new cycle
24
INSERT INTO delinquency_cycles (
25
id,
26
organization_id,
27
customer_id,
28
start_due_date,
29
started_on_utc,
30
status_id,
31
created_on_utc,
32
created_by,
33
is_deleted
34
)
35
VALUES (
36
gen_random_uuid(),
37
p_organization_id,
38
p_customer_id,
39
p_start_due_date,
40
CURRENT_TIMESTAMP,
41
CYCLE_STATUS_OPEN,
42
CURRENT_TIMESTAMP,
43
p_created_by,
44
false
45
)
46
RETURNING id INTO v_cycle_id;
47
48
RETURN v_cycle_id;
49
END;
50
$function$
|
|||||
| Function | dblink_exec | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_exec(text, boolean)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_exec$function$
|
|||||
| Function | dblink_get_pkey | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_get_pkey(text)
2
RETURNS SETOF dblink_pkey_results
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_get_pkey$function$
|
|||||
| Function | dblink_build_sql_insert | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_build_sql_insert(text, int2vector, integer, text[], text[])
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_build_sql_insert$function$
|
|||||
| Function | dblink_build_sql_delete | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_build_sql_delete(text, int2vector, integer, text[])
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_build_sql_delete$function$
|
|||||
| Function | dblink_build_sql_update | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_build_sql_update(text, int2vector, integer, text[], text[])
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_build_sql_update$function$
|
|||||
| Function | dblink_current_query | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_current_query()
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED
5
AS '$libdir/dblink', $function$dblink_current_query$function$
|
|||||
| Function | dblink_send_query | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_send_query(text, text)
2
RETURNS integer
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_send_query$function$
|
|||||
| Function | dblink_is_busy | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_is_busy(text)
2
RETURNS integer
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_is_busy$function$
|
|||||
| Function | dblink_get_result | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_get_result(text)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_get_result$function$
|
|||||
| Function | dblink_get_result | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_get_result(text, boolean)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_get_result$function$
|
|||||
| Function | dblink_get_connections | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_get_connections()
2
RETURNS text[]
3
LANGUAGE c
4
PARALLEL RESTRICTED
5
AS '$libdir/dblink', $function$dblink_get_connections$function$
|
|||||
| Function | dblink_cancel_query | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_cancel_query(text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_cancel_query$function$
|
|||||
| Function | dblink_error_message | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_error_message(text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_error_message$function$
|
|||||
| Function | dblink_get_notify | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_get_notify(OUT notify_name text, OUT be_pid integer, OUT extra text)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_get_notify$function$
|
|||||
| Function | dblink_get_notify | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_get_notify(conname text, OUT notify_name text, OUT be_pid integer, OUT extra text)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/dblink', $function$dblink_get_notify$function$
|
|||||
| Function | dblink_fdw_validator | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dblink_fdw_validator(options text[], catalog oid)
2
RETURNS void
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/dblink', $function$dblink_fdw_validator$function$
|
|||||
| Function | get_latest_raised_group_invoice_summary | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_latest_raised_group_invoice_summary(p_company_id uuid DEFAULT NULL::uuid)
2
RETURNS TABLE(id integer, group_invoice_id uuid, group_invoice_number text, total_targeted_revenue numeric, collected_revenue numeric, total_units_count integer, collected_units_count integer)
3
LANGUAGE sql
4
STABLE
5
AS $function$
6
WITH latest_raised_group_invoice AS (
7
SELECT
8
gih.id,
9
gih.group_invoice_number
10
FROM public.group_invoice_headers gih
11
WHERE gih.is_deleted = false
12
AND (
13
p_company_id IS NULL
14
OR gih.company_id = p_company_id
15
)
16
AND EXISTS (
17
SELECT 1
18
FROM public.group_invoice_details gid
19
WHERE gid.group_invoice_header_id = gih.id
20
AND gid.is_deleted = false
21
)
22
ORDER BY gih.created_on_utc DESC NULLS LAST
23
LIMIT 1
24
)
25
SELECT
26
1 AS id,
27
lgi.id AS group_invoice_id,
28
lgi.group_invoice_number,
29
30
SUM(ih.total_amount) AS total_targeted_revenue,
31
SUM(COALESCE(ih.settled_amount, 0)) AS collected_revenue,
32
33
COUNT(DISTINCT gid.unit_id) AS total_units_count,
34
35
COUNT(
36
DISTINCT CASE
37
WHEN COALESCE(ih.settled_amount, 0) > 0
38
THEN gid.unit_id
39
END
40
) AS collected_units_count
41
42
FROM latest_raised_group_invoice lgi
43
JOIN public.group_invoice_details gid
44
ON gid.group_invoice_header_id = lgi.id
45
AND gid.is_deleted = false
46
JOIN public.invoice_headers ih
47
ON ih.id = gid.invoice_header_id
48
AND ih.is_deleted = false
49
GROUP BY
50
lgi.id,
51
lgi.group_invoice_number;
52
$function$
|
|||||
| Function | get_aging_bucket | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_aging_bucket(p_days integer)
2
RETURNS text
3
LANGUAGE sql
4
IMMUTABLE
5
AS $function$
6
SELECT CASE
7
WHEN p_days <= 30 THEN '0-30'
8
WHEN p_days <= 60 THEN '31-60'
9
WHEN p_days <= 90 THEN '61-90'
10
ELSE '90+'
11
END;
12
$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_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_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 | run_defaulter_fdw | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.run_defaulter_fdw()
2
RETURNS TABLE(result text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
PERFORM public.evaluate_delinquency_for_org('b2756bc9-be6a-42bb-a78a-178ea22b46eb');
7
RETURN QUERY SELECT 'SUCCESS';
8
END;
9
$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
'00000000-0000-0000-0000-000000000000'::uuid
19
);
20
END;
21
$function$
|
|||||
| Function | get_invoice_payment_timeline_by_customerid | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_payment_timeline_by_customerid(p_customer_id uuid, p_company_id uuid DEFAULT NULL::uuid)
2
RETURNS jsonb
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
result jsonb;
7
BEGIN
8
9
SELECT jsonb_agg(
10
jsonb_build_object(
11
'section', section,
12
'dates', dates
13
)
14
)
15
INTO result
16
FROM (
17
SELECT
18
section,
19
jsonb_agg(date_block ORDER BY date_block->>'date') AS dates
20
FROM (
21
SELECT
22
section,
23
jsonb_build_object(
24
'date', event_date,
25
'dateLabel', to_char(event_date, 'Mon DD'),
26
'items',
27
jsonb_agg(
28
jsonb_build_object(
29
'eventType', event_type,
30
'invoiceNumber', invoice_number,
31
'paymentNumber', payment_number,
32
'reference', reference,
33
'title', title,
34
'timeLabel', time_label,
35
'status', status,
36
'amount', amount,
37
'daysToDue', days_to_due,
38
'dueLabel', due_label,
39
'invoiceId', invoice_id,
40
'paymentId', payment_id
41
)
42
ORDER BY event_time
43
)
44
) AS date_block
45
FROM (
46
/* =====================================================
47
INVOICE ISSUED (to resident)
48
====================================================== */
49
SELECT
50
ih.id AS invoice_id,
51
NULL::uuid AS payment_id,
52
ih.invoice_number AS invoice_number,
53
NULL::text AS payment_number,
54
ih.created_on_utc AS event_time,
55
ih.created_on_utc::date AS event_date,
56
'Invoice' AS event_type,
57
ih.invoice_number AS reference,
58
'Invoice Issued' AS title,
59
to_char(ih.created_on_utc, 'HH12:MI AM') AS time_label,
60
ih.total_amount AS amount,
61
62
CASE
63
WHEN ih.settled_amount >= ih.total_amount THEN 'PAID'
64
WHEN ih.settled_amount > 0 THEN 'PARTIAL'
65
WHEN ih.due_date < now() THEN 'OVERDUE'
66
ELSE 'DUE'
67
END AS status,
68
69
CASE
70
WHEN ih.settled_amount >= ih.total_amount THEN NULL
71
ELSE (ih.due_date::date - current_date)
72
END AS days_to_due,
73
74
CASE
75
WHEN ih.settled_amount >= ih.total_amount THEN NULL
76
WHEN ih.due_date::date - current_date < 0
77
THEN concat('Overdue by ', abs(ih.due_date::date - current_date), ' days')
78
ELSE concat('Due in ', ih.due_date::date - current_date, ' days')
79
END AS due_label,
80
81
CASE
82
WHEN ih.settled_amount >= ih.total_amount THEN 'Settled'
83
ELSE 'Open'
84
END AS section
85
86
FROM invoice_headers ih
87
WHERE ih.customer_id = p_customer_id
88
AND ih.is_deleted = false
89
AND (p_company_id IS NULL OR ih.company_id = p_company_id)
90
91
UNION ALL
92
93
/* =====================================================
94
PAYMENT MADE (by resident)
95
====================================================== */
96
SELECT
97
ipd.invoice_header_id AS invoice_id,
98
iph.id AS payment_id,
99
ih.invoice_number AS invoice_number,
100
iph.payment_number AS payment_number,
101
iph.received_date AS event_time,
102
iph.received_date::date AS event_date,
103
'Payment' AS event_type,
104
iph.payment_number AS reference,
105
106
CASE
107
WHEN ih.settled_amount >= ih.total_amount
108
THEN 'Invoice Paid'
109
ELSE 'Partial Payment Made'
110
END AS title,
111
112
to_char(iph.received_date, 'HH12:MI AM') AS time_label,
113
ipd.received_amount AS amount,
114
115
'PAYMENT' AS status,
116
NULL::int AS days_to_due,
117
NULL::text AS due_label,
118
119
CASE
120
WHEN ih.settled_amount >= ih.total_amount THEN 'Settled'
121
ELSE 'Open'
122
END AS section
123
124
FROM invoice_payment_details ipd
125
JOIN invoice_payment_headers iph
126
ON iph.id = ipd.invoice_payment_header_id
127
AND iph.is_deleted = false
128
JOIN invoice_headers ih
129
ON ih.id = ipd.invoice_header_id
130
AND ih.is_deleted = false
131
WHERE iph.customer_id = p_customer_id
132
AND ipd.is_deleted = false
133
AND (p_company_id IS NULL OR iph.company_id = p_company_id)
134
135
) timeline_events
136
GROUP BY section, event_date
137
) grouped_dates
138
GROUP BY section
139
) final_sections;
140
141
RETURN COALESCE(result, '[]'::jsonb);
142
END;
143
$function$
|
|||||
| Function | get_invoice_payment_timeline_by_customerid | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_invoice_payment_timeline_by_customerid(p_customer_id uuid, p_company_id uuid, p_financial_year_start integer)
2
RETURNS jsonb
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
result jsonb;
7
v_financial_year_start date;
8
v_financial_year_end date;
9
BEGIN
10
/* ---------------------------------------------------------
11
Financial Year Window (India: Apr 1 → Mar 31)
12
--------------------------------------------------------- */
13
v_financial_year_start := TO_DATE(p_financial_year_start || '-04-01', 'YYYY-MM-DD');
14
v_financial_year_end := TO_DATE((p_financial_year_start + 1) || '-03-31', 'YYYY-MM-DD');
15
16
/* ---------------------------------------------------------
17
Timeline JSON
18
--------------------------------------------------------- */
19
SELECT jsonb_agg(
20
jsonb_build_object(
21
'section', section,
22
'dates', dates
23
)
24
)
25
INTO result
26
FROM (
27
SELECT
28
section,
29
jsonb_agg(date_block ORDER BY date_block->>'date') AS dates
30
FROM (
31
SELECT
32
section,
33
jsonb_build_object(
34
'date', event_date,
35
'dateLabel', to_char(event_date, 'Mon DD'),
36
'items',
37
jsonb_agg(
38
jsonb_build_object(
39
'eventType', event_type,
40
'invoiceNumber', invoice_number,
41
'paymentNumber', payment_number,
42
'reference', reference,
43
'title', title,
44
'timeLabel', time_label,
45
'status', status,
46
'amount', amount,
47
'daysToDue', days_to_due,
48
'dueLabel', due_label,
49
'invoiceId', invoice_id,
50
'paymentId', payment_id
51
)
52
ORDER BY event_time
53
)
54
) AS date_block
55
FROM (
56
/* =====================================================
57
INVOICE ISSUED (Apartment → Resident)
58
====================================================== */
59
SELECT
60
ih.id AS invoice_id,
61
NULL::uuid AS payment_id,
62
ih.invoice_number,
63
NULL::text AS payment_number,
64
ih.created_on_utc AS event_time,
65
ih.due_date::date AS event_date,
66
'Invoice' AS event_type,
67
ih.invoice_number AS reference,
68
'Invoice Issued' AS title,
69
to_char(ih.created_on_utc, 'HH12:MI AM') AS time_label,
70
ih.total_amount AS amount,
71
72
CASE
73
WHEN ih.settled_amount >= ih.total_amount THEN 'PAID'
74
WHEN ih.settled_amount > 0 THEN 'PARTIAL'
75
WHEN ih.due_date < current_date THEN 'OVERDUE'
76
ELSE 'DUE'
77
END AS status,
78
79
CASE
80
WHEN ih.settled_amount >= ih.total_amount THEN NULL
81
ELSE (ih.due_date::date - current_date)
82
END AS days_to_due,
83
84
CASE
85
WHEN ih.settled_amount >= ih.total_amount THEN NULL
86
WHEN ih.due_date::date < current_date
87
THEN 'Overdue by ' || abs(ih.due_date::date - current_date) || ' days'
88
ELSE 'Due in ' || (ih.due_date::date - current_date) || ' days'
89
END AS due_label,
90
91
CASE
92
WHEN ih.settled_amount >= ih.total_amount THEN 'Settled'
93
ELSE 'Open'
94
END AS section
95
96
FROM invoice_headers ih
97
WHERE ih.customer_id = p_customer_id
98
AND ih.company_id = p_company_id
99
AND ih.is_deleted = false
100
AND ih.due_date::date BETWEEN v_financial_year_start AND v_financial_year_end
101
102
UNION ALL
103
104
/* =====================================================
105
PAYMENT MADE (Resident → Apartment)
106
====================================================== */
107
SELECT
108
ipd.invoice_header_id AS invoice_id,
109
iph.id AS payment_id,
110
ih.invoice_number,
111
iph.payment_number,
112
iph.received_date AS event_time,
113
iph.received_date::date AS event_date,
114
'Payment' AS event_type,
115
iph.payment_number AS reference,
116
117
CASE
118
WHEN ih.settled_amount >= ih.total_amount
119
THEN 'Invoice Paid'
120
ELSE 'Partial Payment Made'
121
END AS title,
122
123
to_char(iph.received_date, 'HH12:MI AM') AS time_label,
124
ipd.received_amount AS amount,
125
126
'PAYMENT' AS status,
127
NULL::int AS days_to_due,
128
NULL::text AS due_label,
129
130
CASE
131
WHEN ih.settled_amount >= ih.total_amount THEN 'Settled'
132
ELSE 'Open'
133
END AS section
134
135
FROM invoice_payment_details ipd
136
JOIN invoice_payment_headers iph
137
ON iph.id = ipd.invoice_payment_header_id
138
AND iph.is_deleted = false
139
JOIN invoice_headers ih
140
ON ih.id = ipd.invoice_header_id
141
AND ih.is_deleted = false
142
WHERE iph.customer_id = p_customer_id
143
AND iph.company_id = p_company_id
144
AND ipd.is_deleted = false
145
AND iph.received_date::date BETWEEN v_financial_year_start AND v_financial_year_end
146
147
) timeline_events
148
GROUP BY section, event_date
149
) grouped_dates
150
GROUP BY section
151
) final_sections;
152
153
RETURN COALESCE(result, '[]'::jsonb);
154
END;
155
$function$
|
|||||
| Function | evaluate_delinquency_for_org | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.evaluate_delinquency_for_org(p_organization_id uuid)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_grace_days integer := 0;
7
v_company_ids uuid[];
8
v_system_user_id uuid;
9
BEGIN
10
/* ====================================================
11
1) Load latest collection policy
12
==================================================== */
13
SELECT COALESCE(cp.grace_days, 0)
14
INTO v_grace_days
15
FROM public.collection_policies cp
16
WHERE cp.organization_id = p_organization_id
17
AND cp.is_deleted = false
18
ORDER BY cp.created_on_utc DESC
19
LIMIT 1;
20
21
/* ====================================================
22
2) Fetch companies for organization
23
==================================================== */
24
SELECT COALESCE(array_agg(c.id), ARRAY[]::uuid[])
25
INTO v_company_ids
26
FROM public.companies c
27
WHERE c.organization_id = p_organization_id
28
AND c.is_deleted = false;
29
30
IF array_length(v_company_ids, 1) IS NULL THEN
31
RETURN;
32
END IF;
33
34
/* ====================================================
35
3) Resolve system user
36
==================================================== */
37
v_system_user_id := public.get_system_user_id();
38
39
/* ====================================================
40
4) Cleanup temp tables (safe for same session)
41
==================================================== */
42
DROP TABLE IF EXISTS tmp_overdue_invoices;
43
DROP TABLE IF EXISTS tmp_calc;
44
DROP TABLE IF EXISTS tmp_open_cycles;
45
DROP TABLE IF EXISTS tmp_cycles_created;
46
DROP TABLE IF EXISTS tmp_cycle_map;
47
DROP TABLE IF EXISTS tmp_snapshot_inserted;
48
DROP TABLE IF EXISTS tmp_active_snapshots;
49
50
/* ====================================================
51
5) MATERIALIZE overdue invoices
52
==================================================== */
53
CREATE TEMP TABLE tmp_overdue_invoices ON COMMIT DROP AS
54
SELECT
55
ih.company_id,
56
ih.customer_id,
57
ih.id AS invoice_id,
58
ih.invoice_date,
59
ih.due_date::date AS due_date,
60
ih.total_amount,
61
COALESCE(ih.settled_amount, 0) AS settled_amount,
62
(ih.total_amount - COALESCE(ih.settled_amount, 0)) AS outstanding_amount
63
FROM public.invoice_headers ih
64
WHERE ih.company_id = ANY (v_company_ids)
65
AND ih.is_deleted = false
66
AND (ih.total_amount - COALESCE(ih.settled_amount, 0)) > 0
67
AND (ih.due_date::date + v_grace_days) < CURRENT_DATE;
68
69
/* ====================================================
70
6) Aggregate delinquency per customer
71
==================================================== */
72
CREATE TEMP TABLE tmp_calc ON COMMIT DROP AS
73
SELECT
74
customer_id,
75
MIN(due_date) AS oldest_due_date,
76
SUM(outstanding_amount) AS overdue_amount,
77
(CURRENT_DATE - MIN(due_date + v_grace_days))::int AS days_past_due,
78
public.get_aging_bucket(
79
(CURRENT_DATE - MIN(due_date + v_grace_days))::int
80
) AS aging_bucket
81
FROM tmp_overdue_invoices
82
GROUP BY customer_id;
83
84
/* ====================================================
85
7) Existing open cycles
86
==================================================== */
87
CREATE TEMP TABLE tmp_open_cycles ON COMMIT DROP AS
88
SELECT
89
dc.customer_id,
90
dc.id AS cycle_id
91
FROM public.delinquency_cycles dc
92
WHERE dc.organization_id = p_organization_id
93
AND dc.ended_on_utc IS NULL
94
AND dc.is_deleted = false;
95
96
/* ====================================================
97
8) Create missing cycles (CTE bridge)
98
==================================================== */
99
CREATE TEMP TABLE tmp_cycles_created ON COMMIT DROP AS
100
WITH ins AS (
101
INSERT INTO public.delinquency_cycles (
102
id, organization_id, customer_id,
103
start_due_date, started_on_utc,
104
ended_on_utc,
105
status_id, resolution_state_id,
106
created_by, created_on_utc, is_deleted
107
)
108
SELECT
109
gen_random_uuid(),
110
p_organization_id,
111
c.customer_id,
112
c.oldest_due_date,
113
now(),
114
NULL,
115
1, -- status id
116
1, -- resolution status id 1
117
v_system_user_id,
118
now(),
119
false
120
FROM tmp_calc c
121
LEFT JOIN tmp_open_cycles oc ON oc.customer_id = c.customer_id
122
WHERE oc.cycle_id IS NULL
123
RETURNING customer_id, id AS cycle_id
124
)
125
SELECT * FROM ins;
126
127
/* ====================================================
128
9) Cycle map (existing + created)
129
==================================================== */
130
CREATE TEMP TABLE tmp_cycle_map ON COMMIT DROP AS
131
SELECT
132
c.customer_id,
133
COALESCE(oc.cycle_id, cc.cycle_id) AS cycle_id,
134
c.oldest_due_date,
135
c.overdue_amount,
136
c.days_past_due,
137
c.aging_bucket
138
FROM tmp_calc c
139
LEFT JOIN tmp_open_cycles oc ON oc.customer_id = c.customer_id
140
LEFT JOIN tmp_cycles_created cc ON cc.customer_id = c.customer_id;
141
142
/* ====================================================
143
10) Insert snapshot if missing (CTE bridge)
144
==================================================== */
145
CREATE TEMP TABLE tmp_snapshot_inserted ON COMMIT DROP AS
146
WITH ins AS (
147
INSERT INTO public.delinquency_snapshots (
148
id, organization_id, customer_id, cycle_id,
149
overdue_amount, oldest_due_date,
150
days_past_due, aging_bucket,
151
last_evaluated_on,
152
created_by, created_on_utc,
153
is_deleted
154
)
155
SELECT
156
gen_random_uuid(),
157
p_organization_id,
158
cm.customer_id,
159
cm.cycle_id,
160
cm.overdue_amount,
161
cm.oldest_due_date,
162
cm.days_past_due,
163
cm.aging_bucket,
164
now(),
165
v_system_user_id,
166
now(),
167
false
168
FROM tmp_cycle_map cm
169
WHERE NOT EXISTS (
170
SELECT 1
171
FROM public.delinquency_snapshots ds
172
WHERE ds.cycle_id = cm.cycle_id
173
AND ds.is_deleted = false
174
)
175
RETURNING cycle_id, id AS snapshot_id
176
)
177
SELECT * FROM ins;
178
179
/* ====================================================
180
11) Resolve active snapshot per cycle
181
==================================================== */
182
CREATE TEMP TABLE tmp_active_snapshots ON COMMIT DROP AS
183
SELECT
184
cm.customer_id,
185
cm.cycle_id,
186
COALESCE(
187
tsi.snapshot_id,
188
(
189
SELECT ds.id
190
FROM public.delinquency_snapshots ds
191
WHERE ds.cycle_id = cm.cycle_id
192
AND ds.is_deleted = false
193
ORDER BY ds.created_on_utc DESC
194
LIMIT 1
195
)
196
) AS snapshot_id
197
FROM tmp_cycle_map cm
198
LEFT JOIN tmp_snapshot_inserted tsi ON tsi.cycle_id = cm.cycle_id;
199
200
/* ====================================================
201
12) Update active snapshots
202
==================================================== */
203
UPDATE public.delinquency_snapshots ds
204
SET overdue_amount = c.overdue_amount,
205
oldest_due_date = c.oldest_due_date,
206
days_past_due = c.days_past_due,
207
aging_bucket = c.aging_bucket,
208
last_evaluated_on = now(),
209
modified_on_utc = now(),
210
modified_by = v_system_user_id
211
FROM tmp_cycle_map c
212
JOIN tmp_active_snapshots a ON a.cycle_id = c.cycle_id
213
WHERE ds.id = a.snapshot_id
214
AND ds.is_deleted = false;
215
216
/* ====================================================
217
13) Refresh snapshot invoices (UPSERT – idempotent)
218
==================================================== */
219
220
-- Soft delete old rows
221
UPDATE public.delinquency_snapshot_invoices dsi
222
SET is_deleted = true,
223
deleted_on_utc = now(),
224
modified_on_utc = now(),
225
modified_by = v_system_user_id
226
WHERE dsi.organization_id = p_organization_id
227
AND dsi.is_deleted = false
228
AND EXISTS (
229
SELECT 1
230
FROM tmp_active_snapshots a
231
WHERE a.snapshot_id = dsi.snapshot_id
232
);
233
234
-- Insert / revive latest overdue invoices
235
INSERT INTO public.delinquency_snapshot_invoices (
236
id,
237
organization_id,
238
company_id,
239
snapshot_id,
240
invoice_id,
241
invoice_date,
242
due_date,
243
total_amount,
244
settled_amount,
245
outstanding_amount,
246
created_on_utc,
247
created_by,
248
modified_on_utc,
249
modified_by,
250
is_deleted,
251
deleted_on_utc
252
)
253
SELECT
254
gen_random_uuid(),
255
p_organization_id,
256
oi.company_id,
257
a.snapshot_id,
258
oi.invoice_id,
259
oi.invoice_date,
260
oi.due_date,
261
oi.total_amount,
262
oi.settled_amount,
263
oi.outstanding_amount,
264
now(),
265
v_system_user_id,
266
now(),
267
v_system_user_id,
268
false,
269
NULL
270
FROM tmp_overdue_invoices oi
271
JOIN tmp_active_snapshots a
272
ON a.customer_id = oi.customer_id
273
GROUP BY
274
oi.company_id,
275
a.snapshot_id,
276
oi.invoice_id,
277
oi.invoice_date,
278
oi.due_date,
279
oi.total_amount,
280
oi.settled_amount,
281
oi.outstanding_amount
282
ON CONFLICT (snapshot_id, invoice_id)
283
DO UPDATE
284
SET
285
invoice_date = EXCLUDED.invoice_date,
286
due_date = EXCLUDED.due_date,
287
total_amount = EXCLUDED.total_amount,
288
settled_amount = EXCLUDED.settled_amount,
289
outstanding_amount = EXCLUDED.outstanding_amount,
290
is_deleted = false,
291
deleted_on_utc = NULL,
292
modified_on_utc = now(),
293
modified_by = v_system_user_id;
294
295
/* ====================================================
296
14) Close cycles with no overdue invoices
297
==================================================== */
298
UPDATE public.delinquency_cycles dc
299
SET ended_on_utc = now(),
300
status_id = 2,
301
modified_on_utc = now(),
302
modified_by = v_system_user_id
303
WHERE dc.organization_id = p_organization_id
304
AND dc.ended_on_utc IS NULL
305
AND dc.is_deleted = false
306
AND NOT EXISTS (
307
SELECT 1
308
FROM tmp_calc c
309
WHERE c.customer_id = dc.customer_id
310
);
311
312
/* ====================================================
313
15) Cleanup snapshots & windows for closed cycles
314
==================================================== */
315
UPDATE public.delinquency_snapshots ds
316
SET is_deleted = true,
317
deleted_on_utc = now(),
318
modified_on_utc = now(),
319
modified_by = v_system_user_id
320
WHERE ds.organization_id = p_organization_id
321
AND ds.is_deleted = false
322
AND EXISTS (
323
SELECT 1
324
FROM public.delinquency_cycles dc
325
WHERE dc.id = ds.cycle_id
326
AND dc.ended_on_utc IS NOT NULL
327
AND dc.is_deleted = false
328
);
329
330
UPDATE public.delinquency_resolution_window drw
331
SET is_deleted = true,
332
deleted_on_utc = now(),
333
modified_on_utc = now(),
334
modified_by = v_system_user_id
335
WHERE drw.is_deleted = false
336
AND EXISTS (
337
SELECT 1
338
FROM public.delinquency_cycles dc
339
WHERE dc.id = drw.id
340
AND dc.organization_id = p_organization_id
341
AND dc.ended_on_utc IS NOT NULL
342
AND dc.is_deleted = false
343
);
344
345
END;
346
$function$
|
|||||
| Function | evaluate_delinquency_for_org | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.evaluate_delinquency_for_org(p_organization_id uuid, p_company_id uuid)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_grace_days integer := 0;
7
v_system_user_id uuid;
8
BEGIN
9
/* ====================================================
10
1) Load latest collection policy
11
==================================================== */
12
SELECT COALESCE(cp.grace_days, 0)
13
INTO v_grace_days
14
FROM public.collection_policies cp
15
WHERE cp.organization_id = p_organization_id
16
AND cp.is_deleted = false
17
ORDER BY cp.created_on_utc DESC
18
LIMIT 1;
19
20
21
v_system_user_id := public.get_system_user_id();
22
23
/* ====================================================
24
4) Cleanup temp tables (safe for same session)
25
==================================================== */
26
DROP TABLE IF EXISTS tmp_overdue_invoices;
27
DROP TABLE IF EXISTS tmp_calc;
28
DROP TABLE IF EXISTS tmp_open_cycles;
29
DROP TABLE IF EXISTS tmp_cycles_created;
30
DROP TABLE IF EXISTS tmp_cycle_map;
31
DROP TABLE IF EXISTS tmp_snapshot_inserted;
32
DROP TABLE IF EXISTS tmp_active_snapshots;
33
34
/* ====================================================
35
5) MATERIALIZE overdue invoices
36
==================================================== */
37
CREATE TEMP TABLE tmp_overdue_invoices ON COMMIT DROP AS
38
SELECT
39
company_id,
40
customer_id,
41
invoice_id,
42
invoice_date,
43
due_date,
44
total_amount,
45
settled_amount,
46
outstanding_amount
47
FROM public.get_canonical_overdue_invoices(p_company_id);
48
49
/* ====================================================
50
6) Aggregate delinquency per customer
51
==================================================== */
52
CREATE TEMP TABLE tmp_calc ON COMMIT DROP AS
53
SELECT
54
customer_id,
55
MIN(due_date) AS oldest_due_date,
56
SUM(outstanding_amount) AS overdue_amount,
57
(CURRENT_DATE - MIN(due_date + v_grace_days))::int AS days_past_due,
58
public.get_aging_bucket(
59
(CURRENT_DATE - MIN(due_date + v_grace_days))::int
60
) AS aging_bucket
61
FROM tmp_overdue_invoices
62
GROUP BY customer_id;
63
64
/* ====================================================
65
7) Existing open cycles
66
==================================================== */
67
CREATE TEMP TABLE tmp_open_cycles ON COMMIT DROP AS
68
SELECT
69
dc.customer_id,
70
dc.id AS cycle_id
71
FROM public.delinquency_cycles dc
72
WHERE dc.company_id = p_company_id
73
AND dc.ended_on_utc IS NULL
74
AND dc.is_deleted = false;
75
76
/* ====================================================
77
8) Create missing cycles (CTE bridge)
78
==================================================== */
79
CREATE TEMP TABLE tmp_cycles_created ON COMMIT DROP AS
80
WITH ins AS (
81
INSERT INTO public.delinquency_cycles (
82
id, organization_id, company_id, customer_id,
83
start_due_date, started_on_utc,
84
ended_on_utc,
85
status_id, resolution_state_id,
86
created_by, created_on_utc, is_deleted
87
)
88
SELECT
89
gen_random_uuid(),
90
p_organization_id,
91
p_company_id,
92
c.customer_id,
93
c.oldest_due_date,
94
now(),
95
NULL,
96
1, -- status id
97
1, -- resolution status id 1
98
v_system_user_id,
99
now(),
100
false
101
FROM tmp_calc c
102
LEFT JOIN tmp_open_cycles oc ON oc.customer_id = c.customer_id
103
WHERE oc.cycle_id IS NULL
104
RETURNING customer_id, id AS cycle_id
105
)
106
SELECT * FROM ins;
107
108
/* ====================================================
109
9) Cycle map (existing + created)
110
==================================================== */
111
CREATE TEMP TABLE tmp_cycle_map ON COMMIT DROP AS
112
SELECT
113
c.customer_id,
114
COALESCE(oc.cycle_id, cc.cycle_id) AS cycle_id,
115
c.oldest_due_date,
116
c.overdue_amount,
117
c.days_past_due,
118
c.aging_bucket
119
FROM tmp_calc c
120
LEFT JOIN tmp_open_cycles oc ON oc.customer_id = c.customer_id
121
LEFT JOIN tmp_cycles_created cc ON cc.customer_id = c.customer_id;
122
123
/* ====================================================
124
10) Insert snapshot if missing (CTE bridge)
125
==================================================== */
126
CREATE TEMP TABLE tmp_snapshot_inserted ON COMMIT DROP AS
127
WITH ins AS (
128
INSERT INTO public.delinquency_snapshots (
129
id, organization_id, company_id, customer_id, cycle_id,
130
overdue_amount, oldest_due_date,
131
days_past_due, aging_bucket,
132
last_evaluated_on,
133
created_by, created_on_utc,
134
is_deleted
135
)
136
SELECT
137
gen_random_uuid(),
138
p_organization_id,
139
p_company_id,
140
cm.customer_id,
141
cm.cycle_id,
142
cm.overdue_amount,
143
cm.oldest_due_date,
144
cm.days_past_due,
145
cm.aging_bucket,
146
now(),
147
v_system_user_id,
148
now(),
149
false
150
FROM tmp_cycle_map cm
151
WHERE NOT EXISTS (
152
SELECT 1
153
FROM public.delinquency_snapshots ds
154
WHERE ds.cycle_id = cm.cycle_id
155
AND ds.is_deleted = false
156
)
157
RETURNING cycle_id, id AS snapshot_id
158
)
159
SELECT * FROM ins;
160
161
/* ====================================================
162
11) Resolve active snapshot per cycle
163
==================================================== */
164
CREATE TEMP TABLE tmp_active_snapshots ON COMMIT DROP AS
165
SELECT
166
cm.customer_id,
167
cm.cycle_id,
168
COALESCE(
169
tsi.snapshot_id,
170
(
171
SELECT ds.id
172
FROM public.delinquency_snapshots ds
173
WHERE ds.cycle_id = cm.cycle_id
174
AND ds.is_deleted = false
175
ORDER BY ds.created_on_utc DESC
176
LIMIT 1
177
)
178
) AS snapshot_id
179
FROM tmp_cycle_map cm
180
LEFT JOIN tmp_snapshot_inserted tsi ON tsi.cycle_id = cm.cycle_id;
181
182
/* ====================================================
183
12) Update active snapshots
184
==================================================== */
185
UPDATE public.delinquency_snapshots ds
186
SET overdue_amount = c.overdue_amount,
187
oldest_due_date = c.oldest_due_date,
188
days_past_due = c.days_past_due,
189
aging_bucket = c.aging_bucket,
190
last_evaluated_on = now(),
191
modified_on_utc = now(),
192
modified_by = v_system_user_id
193
FROM tmp_cycle_map c
194
JOIN tmp_active_snapshots a ON a.cycle_id = c.cycle_id
195
WHERE ds.id = a.snapshot_id
196
AND ds.is_deleted = false;
197
198
/* ====================================================
199
13) Refresh snapshot invoices (UPSERT – idempotent)
200
==================================================== */
201
202
-- Soft delete old rows
203
UPDATE public.delinquency_snapshot_invoices dsi
204
SET is_deleted = true,
205
deleted_on_utc = now(),
206
modified_on_utc = now(),
207
modified_by = v_system_user_id
208
WHERE dsi.company_id = p_company_id
209
AND dsi.is_deleted = false
210
AND EXISTS (
211
SELECT 1
212
FROM tmp_active_snapshots a
213
WHERE a.snapshot_id = dsi.snapshot_id
214
);
215
216
-- Insert / revive latest overdue invoices
217
INSERT INTO public.delinquency_snapshot_invoices (
218
id,
219
company_id,
220
snapshot_id,
221
invoice_id,
222
invoice_date,
223
due_date,
224
total_amount,
225
settled_amount,
226
outstanding_amount,
227
created_on_utc,
228
created_by,
229
modified_on_utc,
230
modified_by,
231
is_deleted,
232
deleted_on_utc
233
)
234
SELECT
235
gen_random_uuid(),
236
oi.company_id,
237
a.snapshot_id,
238
oi.invoice_id,
239
oi.invoice_date,
240
oi.due_date,
241
oi.total_amount,
242
oi.settled_amount,
243
oi.outstanding_amount,
244
now(),
245
v_system_user_id,
246
now(),
247
v_system_user_id,
248
false,
249
NULL
250
FROM tmp_overdue_invoices oi
251
JOIN tmp_active_snapshots a
252
ON a.customer_id = oi.customer_id
253
GROUP BY
254
oi.company_id,
255
a.snapshot_id,
256
oi.invoice_id,
257
oi.invoice_date,
258
oi.due_date,
259
oi.total_amount,
260
oi.settled_amount,
261
oi.outstanding_amount
262
ON CONFLICT (snapshot_id, invoice_id)
263
DO UPDATE
264
SET
265
invoice_date = EXCLUDED.invoice_date,
266
due_date = EXCLUDED.due_date,
267
total_amount = EXCLUDED.total_amount,
268
settled_amount = EXCLUDED.settled_amount,
269
outstanding_amount = EXCLUDED.outstanding_amount,
270
is_deleted = false,
271
deleted_on_utc = NULL,
272
modified_on_utc = now(),
273
modified_by = v_system_user_id;
274
275
/* ====================================================
276
14) Close cycles with no overdue invoices
277
==================================================== */
278
UPDATE public.delinquency_cycles dc
279
SET ended_on_utc = now(),
280
status_id = 2,
281
modified_on_utc = now(),
282
modified_by = v_system_user_id
283
WHERE dc.organization_id = p_organization_id
284
AND dc.company_id = p_company_id
285
AND dc.ended_on_utc IS NULL
286
AND dc.is_deleted = false
287
AND NOT EXISTS (
288
SELECT 1
289
FROM tmp_calc c
290
WHERE c.customer_id = dc.customer_id
291
);
292
293
/* ====================================================
294
15) Cleanup snapshots & windows for closed cycles
295
==================================================== */
296
UPDATE public.delinquency_snapshots ds
297
SET is_deleted = true,
298
deleted_on_utc = now(),
299
modified_on_utc = now(),
300
modified_by = v_system_user_id
301
WHERE ds.organization_id = p_organization_id
302
AND ds.company_id = p_company_id
303
AND ds.is_deleted = false
304
AND EXISTS (
305
SELECT 1
306
FROM public.delinquency_cycles dc
307
WHERE dc.id = ds.cycle_id
308
AND dc.ended_on_utc IS NOT NULL
309
AND dc.is_deleted = false
310
);
311
312
UPDATE public.delinquency_resolution_window drw
313
SET is_deleted = true,
314
deleted_on_utc = now(),
315
modified_on_utc = now(),
316
modified_by = v_system_user_id
317
WHERE drw.is_deleted = false
318
AND EXISTS (
319
SELECT 1
320
FROM public.delinquency_cycles dc
321
WHERE dc.id = drw.id
322
AND dc.organization_id = p_organization_id
323
AND dc.company_id = p_company_id
324
AND dc.ended_on_utc IS NOT NULL
325
AND dc.is_deleted = false
326
);
327
328
END;
329
$function$
|
|||||
| Function | create_delinquency_action | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.create_delinquency_action(p_organization_id uuid, p_customer_id uuid, p_cycle_id uuid, p_action_type_code text, p_action_on_utc timestamp with time zone, p_actor uuid, p_resolution_state_code text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_meta jsonb DEFAULT NULL::jsonb)
2
RETURNS jsonb
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_action_type_id int;
7
v_resolution_state_id int;
8
v_action_id uuid := gen_random_uuid();
9
v_snapshot_id uuid;
10
BEGIN
11
-- Validate cycle belongs to org + customer and is open
12
IF NOT EXISTS (
13
SELECT 1
14
FROM public.delinquency_cycles dc
15
WHERE dc.id = p_cycle_id
16
AND dc.organization_id = p_organization_id
17
AND dc.customer_id = p_customer_id
18
AND dc.is_deleted = false
19
) THEN
20
RAISE EXCEPTION 'Invalid cycle for org/customer';
21
END IF;
22
23
-- Resolve action type
24
SELECT dat.id INTO v_action_type_id
25
FROM public.delinquency_action_types dat
26
WHERE dat.code = p_action_type_code;
27
28
IF v_action_type_id IS NULL THEN
29
RAISE EXCEPTION 'Unknown action type code %', p_action_type_code;
30
END IF;
31
32
-- Resolve resolution state
33
IF p_resolution_state_code IS NOT NULL THEN
34
SELECT drs.id INTO v_resolution_state_id
35
FROM public.delinquency_resolution_states drs
36
WHERE drs.code = p_resolution_state_code;
37
END IF;
38
39
-- Attach current active snapshot (if any)
40
SELECT ds.id INTO v_snapshot_id
41
FROM public.delinquency_snapshots ds
42
WHERE ds.cycle_id = p_cycle_id
43
AND ds.is_deleted = false
44
ORDER BY ds.created_on_utc DESC
45
LIMIT 1;
46
47
-- Insert action
48
INSERT INTO public.delinquency_actions (
49
id, organization_id, customer_id, cycle_id, snapshot_id,
50
action_type_id, resolution_state_id,
51
notes, meta, action_on_utc,
52
created_by, created_on_utc, is_deleted
53
)
54
VALUES (
55
v_action_id, p_organization_id, p_customer_id, p_cycle_id, v_snapshot_id,
56
v_action_type_id, v_resolution_state_id,
57
p_notes, p_meta, p_action_on_utc,
58
p_actor, now(), false
59
);
60
61
-- Update cycle resolution state if provided
62
IF v_resolution_state_id IS NOT NULL THEN
63
UPDATE public.delinquency_cycles
64
SET resolution_state_id = v_resolution_state_id,
65
modified_on_utc = now(),
66
modified_by = p_actor
67
WHERE id = p_cycle_id;
68
END IF;
69
70
-- Upsert resolution window if TIME_GRANTED or PROMISE_TO_PAY
71
IF p_resolution_state_code IN ('TIME_GRANTED', 'PROMISE_TO_PAY') THEN
72
INSERT INTO public.delinquency_resolution_window (
73
cycle_id, pause_evaluation_until, reason,
74
created_on_utc, created_by, is_deleted
75
)
76
VALUES (
77
p_cycle_id,
78
COALESCE(
79
(p_meta ->> 'expectedDate')::date,
80
(p_meta ->> 'promiseDate')::date,
81
CURRENT_DATE
82
),
83
p_notes,
84
now(),
85
p_actor,
86
false
87
)
88
ON CONFLICT (cycle_id) DO UPDATE
89
SET pause_evaluation_until = EXCLUDED.pause_evaluation_until,
90
reason = COALESCE(EXCLUDED.reason, public.delinquency_resolution_window.reason),
91
is_deleted = false,
92
modified_by = p_actor,
93
modified_on_utc = now();
94
END IF;
95
96
-- If SETTLED or WAIVED -> close cycle + deactivate snapshot data
97
IF p_resolution_state_code IN ('SETTLED', 'WAIVED') THEN
98
UPDATE public.delinquency_cycles
99
SET ended_on_utc = now(),
100
status_id = 2,
101
modified_on_utc = now(),
102
modified_by = p_actor
103
WHERE id = p_cycle_id
104
AND ended_on_utc IS NULL;
105
106
-- Soft delete active snapshot
107
UPDATE public.delinquency_snapshots
108
SET is_deleted = true,
109
deleted_on_utc = now(),
110
deleted_by = p_actor
111
WHERE cycle_id = p_cycle_id
112
AND is_deleted = false;
113
114
-- Soft delete snapshot invoices
115
UPDATE public.delinquency_snapshot_invoices
116
SET is_deleted = true,
117
deleted_on_utc = now(),
118
deleted_by = p_actor
119
WHERE snapshot_id = v_snapshot_id
120
AND is_deleted = false;
121
122
-- Soft delete resolution window
123
UPDATE public.delinquency_resolution_window
124
SET is_deleted = true,
125
deleted_on_utc = now(),
126
deleted_by = p_actor
127
WHERE cycle_id = p_cycle_id
128
AND is_deleted = false;
129
END IF;
130
131
RETURN jsonb_build_object(
132
'action',
133
(SELECT row_to_json(a)
134
FROM (
135
SELECT da.id, da.organization_id, da.customer_id, da.cycle_id, da.snapshot_id,
136
dat.code AS action_type_code,
137
drs.code AS resolution_state_code,
138
da.notes, da.meta, da.action_on_utc
139
FROM public.delinquency_actions da
140
JOIN public.delinquency_action_types dat ON dat.id = da.action_type_id
141
LEFT JOIN public.delinquency_resolution_states drs ON drs.id = da.resolution_state_id
142
WHERE da.id = v_action_id
143
) a),
144
'cycle',
145
(SELECT row_to_json(c)
146
FROM (
147
SELECT dc.id, dc.resolution_state_id, dc.ended_on_utc, dc.status_id
148
FROM public.delinquency_cycles dc
149
WHERE dc.id = p_cycle_id
150
) c)
151
);
152
END;
153
$function$
|
|||||
| Function | create_discussion_message | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.create_discussion_message(p_organization_id uuid, p_cycle_id uuid, p_sender_id uuid, p_sender_type text, p_message_text text, p_sent_on_utc timestamp with time zone, p_actor uuid)
2
RETURNS jsonb
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_message_id uuid := gen_random_uuid();
7
v_result jsonb;
8
BEGIN
9
-- Verify the cycle exists and belongs to the organization
10
IF NOT EXISTS(
11
SELECT 1
12
FROM public.delinquency_cycles dc
13
WHERE dc.id = p_cycle_id
14
AND dc.organization_id = p_organization_id
15
AND dc.is_deleted = false
16
) THEN
17
RAISE EXCEPTION 'Delinquency cycle % not found or does not belong to organization %', p_cycle_id, p_organization_id;
18
END IF;
19
20
-- Validate sender_type
21
IF p_sender_type NOT IN ('RESIDENT', 'FACILITY_MANAGER') THEN
22
RAISE EXCEPTION 'Invalid sender_type: %. Must be RESIDENT or FACILITY_MANAGER', p_sender_type;
23
END IF;
24
25
-- Insert the message
26
INSERT INTO public.delinquency_discussion_messages (
27
id,
28
organization_id,
29
cycle_id,
30
sender_id,
31
sender_type,
32
message_text,
33
sent_on_utc,
34
created_by,
35
created_on_utc,
36
is_deleted
37
)
38
VALUES (
39
v_message_id,
40
p_organization_id,
41
p_cycle_id,
42
p_sender_id,
43
p_sender_type,
44
p_message_text,
45
p_sent_on_utc,
46
p_actor,
47
now(),
48
false
49
);
50
51
-- Return the created message
52
v_result := jsonb_build_object(
53
'message',
54
(SELECT row_to_json(msg)
55
FROM (SELECT ddm.id,
56
ddm.cycle_id,
57
ddm.sender_id,
58
ddm.sender_type,
59
ddm.message_text,
60
ddm.sent_on_utc
61
FROM public.delinquency_discussion_messages ddm
62
WHERE ddm.id = v_message_id) msg)
63
);
64
65
RETURN v_result;
66
END;
67
$function$
|
|||||
| Function | get_defaulter_contact | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_defaulter_contact(p_customer_id uuid)
2
RETURNS text
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_contact_number text;
7
BEGIN
8
SELECT
9
r.contact_number INTO v_contact_number
10
FROM
11
fdw_community.units u
12
INNER JOIN
13
fdw_community.resident_units ru ON u.id = ru.unit_id
14
INNER JOIN
15
fdw_community.residents r ON ru.resident_id = r.id
16
WHERE
17
u.customer_id = p_customer_id
18
AND u.is_deleted = false
19
AND ru.is_deleted = false
20
AND r.is_deleted = false
21
AND r.contact_number IS NOT NULL
22
LIMIT 1;
23
24
RETURN v_contact_number;
25
END;
26
$function$
|
|||||
| Function | get_defaulter_list | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_defaulter_list(p_company_id uuid)
2
RETURNS TABLE(id integer, cycle_id uuid, customer_id uuid, snapshot_id uuid, overdue_amount numeric, days_past_due integer, aging_bucket text, resolution_state text, is_anonymized boolean, can_reveal_unit boolean, next_expected_action_date date, customer_name text, resident_contact text, oldest_due_date date)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_organization_id uuid;
7
BEGIN
8
/* ------------------------------------------------------------
9
1️⃣ Resolve organization_id
10
------------------------------------------------------------ */
11
SELECT c.organization_id
12
INTO v_organization_id
13
FROM public.companies c
14
WHERE c.id = p_company_id;
15
16
/* ------------------------------------------------------------
17
2️⃣ Main query
18
------------------------------------------------------------ */
19
RETURN QUERY
20
WITH policy AS (
21
SELECT
22
COALESCE(cp.grace_days, 0)::integer AS grace_days,
23
COALESCE(cp.anonymize_until_days, 0)::integer AS anonymize_until_days,
24
COALESCE(cp.reveal_unit_after_days, 0)::integer AS reveal_unit_after_days
25
FROM public.collection_policies cp
26
WHERE cp.organization_id = v_organization_id
27
AND cp.is_deleted = false
28
ORDER BY cp.created_on_utc DESC
29
LIMIT 1
30
),
31
32
open_cycles AS (
33
SELECT
34
dc.id,
35
dc.customer_id,
36
dc.resolution_state_id
37
FROM public.delinquency_cycles dc
38
WHERE dc.company_id = p_company_id
39
AND dc.ended_on_utc IS NULL
40
AND dc.is_deleted = false
41
),
42
43
active_snapshots AS (
44
SELECT
45
ds.id,
46
ds.cycle_id,
47
ds.overdue_amount,
48
ds.days_past_due,
49
ds.aging_bucket::text,
50
ds.oldest_due_date::date
51
FROM public.delinquency_snapshots ds
52
WHERE ds.company_id = p_company_id
53
AND ds.is_deleted = false
54
),
55
56
last_action AS (
57
SELECT DISTINCT ON (da.cycle_id)
58
da.cycle_id,
59
da.meta,
60
da.action_on_utc
61
FROM public.delinquency_actions da
62
WHERE da.company_id = p_company_id
63
AND da.is_deleted = false
64
ORDER BY da.cycle_id, da.action_on_utc DESC
65
),
66
67
base AS (
68
SELECT
69
/* row_number() → bigint → integer */
70
row_number() OVER (
71
ORDER BY ds.days_past_due DESC, ds.overdue_amount DESC
72
)::integer AS row_no,
73
74
oc.id AS cycle_id,
75
oc.customer_id,
76
ds.id AS snapshot_id,
77
ds.overdue_amount,
78
ds.days_past_due,
79
ds.aging_bucket,
80
rs.code::text AS resolution_state_code,
81
82
/* Policy-driven flags */
83
(ds.days_past_due < COALESCE(p.anonymize_until_days, 0)) AS is_anonymized,
84
(ds.days_past_due >= COALESCE(p.reveal_unit_after_days, 0)) AS can_reveal_unit,
85
86
/* JSON → text → date */
87
NULLIF(la.meta ->> 'expectedDate', '')::date
88
AS next_expected_action_date,
89
90
cust.name::text AS customer_name,
91
r.contact_number::text AS resident_contact,
92
ds.oldest_due_date
93
94
FROM open_cycles oc
95
JOIN active_snapshots ds
96
ON ds.cycle_id = oc.id
97
98
LEFT JOIN policy p ON TRUE
99
LEFT JOIN last_action la ON la.cycle_id = oc.id
100
LEFT JOIN public.delinquency_resolution_states rs
101
ON rs.id = oc.resolution_state_id
102
LEFT JOIN public.customers cust
103
ON cust.id = oc.customer_id
104
105
/* ---------- Community FDW joins ---------- */
106
107
LEFT JOIN fdw_community.units u
108
ON u.customer_id = oc.customer_id
109
AND u.is_deleted = false
110
111
/* Pick exactly ONE resident per unit */
112
LEFT JOIN LATERAL (
113
SELECT DISTINCT ON (ru.unit_id)
114
ru.unit_id,
115
ru.resident_id
116
FROM fdw_community.resident_units ru
117
JOIN fdw_community.residents r2
118
ON r2.id = ru.resident_id
119
AND r2.is_deleted = false
120
AND r2.contact_number IS NOT NULL
121
WHERE ru.unit_id = u.id
122
AND ru.is_deleted = false
123
ORDER BY
124
ru.unit_id,
125
ru.is_primary_owner DESC,
126
ru.created_on_utc ASC
127
) ru ON TRUE
128
129
LEFT JOIN fdw_community.residents r
130
ON r.id = ru.resident_id
131
132
/* Visibility rule */
133
WHERE ds.days_past_due >= COALESCE(p.reveal_unit_after_days, 0)
134
)
135
136
SELECT
137
b.row_no AS id,
138
b.cycle_id,
139
b.customer_id,
140
b.snapshot_id,
141
b.overdue_amount,
142
b.days_past_due,
143
b.aging_bucket,
144
COALESCE(b.resolution_state_code, 'NONE')::text AS resolution_state,
145
b.is_anonymized,
146
b.can_reveal_unit,
147
b.next_expected_action_date,
148
b.customer_name,
149
b.resident_contact,
150
b.oldest_due_date
151
FROM base b
152
ORDER BY
153
b.days_past_due DESC,
154
b.overdue_amount DESC;
155
156
END;
157
$function$
|
|||||
| Function | get_defaulter_details | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_defaulter_details(p_company_id uuid, p_customer_id uuid)
2
RETURNS jsonb
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_cycle_id uuid;
7
v_snapshot_id uuid;
8
BEGIN
9
SELECT dc.id
10
INTO v_cycle_id
11
FROM public.delinquency_cycles dc
12
WHERE dc.company_id = p_company_id
13
AND dc.customer_id = p_customer_id
14
AND dc.ended_on_utc IS NULL
15
AND dc.is_deleted = false
16
ORDER BY dc.started_on_utc DESC
17
LIMIT 1;
18
19
IF v_cycle_id IS NULL THEN
20
RETURN '{}'::jsonb;
21
END IF;
22
23
SELECT ds.id
24
INTO v_snapshot_id
25
FROM public.delinquency_snapshots ds
26
WHERE ds.cycle_id = v_cycle_id
27
AND ds.is_deleted = false
28
ORDER BY ds.created_on_utc DESC
29
LIMIT 1;
30
31
RETURN jsonb_build_object(
32
'cycle',
33
(SELECT row_to_json(c)
34
FROM (
35
SELECT dc.id, dc.organization_id, dc.company_id, dc.customer_id,
36
dc.start_due_date, dc.started_on_utc, dc.ended_on_utc,
37
dc.status_id, dc.resolution_state_id
38
FROM public.delinquency_cycles dc
39
WHERE dc.id = v_cycle_id
40
) c),
41
'snapshot',
42
(SELECT row_to_json(s)
43
FROM (
44
SELECT ds.id, ds.cycle_id, ds.company_id, ds.customer_id,
45
ds.overdue_amount, ds.oldest_due_date,
46
ds.days_past_due, ds.aging_bucket,
47
ds.last_evaluated_on
48
FROM public.delinquency_snapshots ds
49
WHERE ds.id = v_snapshot_id
50
) s),
51
'invoices',
52
(SELECT COALESCE(jsonb_agg(row_to_json(inv)), '[]'::jsonb)
53
FROM (
54
SELECT dsi.invoice_id, dsi.snapshot_id, dsi.company_id,
55
dsi.invoice_date, dsi.due_date,
56
dsi.total_amount, dsi.settled_amount, dsi.outstanding_amount,
57
ih.invoice_number, ih.note
58
FROM public.delinquency_snapshot_invoices dsi
59
JOIN public.invoice_headers ih ON ih.id = dsi.invoice_id
60
WHERE dsi.snapshot_id = v_snapshot_id
61
AND dsi.is_deleted = false
62
) inv),
63
'actions',
64
(SELECT COALESCE(jsonb_agg(row_to_json(act) ORDER BY act.action_on_utc DESC), '[]'::jsonb)
65
FROM (
66
SELECT da.id, da.cycle_id, da.customer_id, da.organization_id, da.snapshot_id,
67
dat.code AS action_type_code,
68
drs.code AS resolution_state_code,
69
da.notes, da.meta, da.action_on_utc
70
FROM public.delinquency_actions da
71
JOIN public.delinquency_action_types dat ON dat.id = da.action_type_id
72
LEFT JOIN public.delinquency_resolution_states drs ON drs.id = da.resolution_state_id
73
WHERE da.cycle_id = v_cycle_id
74
AND da.is_deleted = false
75
) act)
76
);
77
END;
78
$function$
|
|||||
| Function | get_canonical_overdue_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_canonical_overdue_invoices(p_company_id uuid)
2
RETURNS TABLE(company_id uuid, customer_id uuid, invoice_id uuid, invoice_date date, due_date date, total_amount numeric, settled_amount numeric, outstanding_amount numeric, days_past_due integer)
3
LANGUAGE sql
4
STABLE
5
AS $function$
6
SELECT
7
ih.company_id,
8
ih.customer_id,
9
ih.id AS invoice_id,
10
ih.invoice_date::date,
11
ih.due_date::date,
12
ih.total_amount,
13
COALESCE(ih.settled_amount, 0) AS settled_amount,
14
(ih.total_amount - COALESCE(ih.settled_amount, 0)) AS outstanding_amount,
15
(CURRENT_DATE - (ih.due_date::date + cp.grace_days))::int AS days_past_due
16
FROM public.invoice_headers ih
17
JOIN public.companies co
18
ON co.id = ih.company_id
19
AND co.is_deleted = false
20
JOIN public.collection_policies cp
21
ON cp.organization_id = co.organization_id
22
AND cp.is_deleted = false
23
WHERE ih.company_id = p_company_id
24
AND ih.is_deleted = false
25
AND (ih.total_amount - COALESCE(ih.settled_amount, 0)) > 0
26
AND (ih.due_date::date + cp.grace_days) < CURRENT_DATE;
27
$function$
|
|||||
| Function | get_defaulter_summary | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_defaulter_summary(p_company_id uuid)
2
RETURNS TABLE(id integer, total_defaulters_count integer, total_outstanding numeric)
3
LANGUAGE sql
4
STABLE
5
AS $function$
6
SELECT
7
1 AS id,
8
COUNT(DISTINCT customer_id)::int AS total_defaulters_count,
9
SUM(outstanding_amount) AS total_outstanding
10
FROM public.get_canonical_overdue_invoices(p_company_id);
11
$function$
|
|||||
| Procedure | insert_invoice_by_excel | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_invoice_by_excel(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_discount numeric, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_invoice_number text, IN p_payment_term integer, IN p_invoice_status_id integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_note text, IN p_round_off numeric, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_so_no text, IN p_so_date timestamp without time zone, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_invoice_voucher text, IN p_created_by uuid, IN p_invoice_details jsonb, IN p_type integer, IN p_settled_amount numeric DEFAULT 0)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
detail RECORD;
6
v_source_warehouse_id uuid;
7
v_destination_warehouse_id uuid;
8
v_invoice_number text;
9
BEGIN
10
-- Set default values for warehouses
11
v_source_warehouse_id := COALESCE(p_source_warehouse_id, NULL);
12
v_destination_warehouse_id := COALESCE(p_destination_warehouse_id, NULL);
13
14
IF p_invoice_number IS NULL OR p_invoice_number = '' THEN
15
v_invoice_number := get_new_invoice_number(p_company_id, p_invoice_date::date);
16
ELSE
17
v_invoice_number = p_invoice_number;
18
END IF;
19
20
-- Validate JSON structure
21
BEGIN
22
PERFORM jsonb_array_elements(p_invoice_details)::jsonb;
23
EXCEPTION
24
WHEN others THEN
25
RAISE EXCEPTION 'Invalid JSON format for invoice details';
26
END;
27
RAISE NOTICE 'Processing Invoice: %', p_id;
28
-- Insert into the invoice header
29
INSERT INTO
30
public.invoice_headers(
31
id,
32
company_id,
33
customer_id,
34
credit_account_id,
35
debit_account_id,
36
invoice_number,
37
invoice_date,
38
due_date,
39
payment_term,
40
taxable_amount,
41
cgst_amount,
42
sgst_amount,
43
igst_amount,
44
total_amount,
45
discount,
46
fees,
47
round_off,
48
currency_id,
49
note,
50
invoice_status_id,
51
created_by,
52
invoice_voucher,
53
source_warehouse_id,
54
destination_warehouse_id,
55
created_on_utc,
56
so_no,
57
so_date,
58
type
59
)
60
VALUES (
61
p_id,
62
p_company_id,
63
p_customer_id,
64
p_credit_account_id,
65
p_debit_account_id,
66
v_invoice_number,
67
p_invoice_date,
68
p_due_date,
69
p_payment_term,
70
p_taxable_amount,
71
p_cgst_amount,
72
p_sgst_amount,
73
p_igst_amount,
74
p_total_amount,
75
p_discount,
76
p_fees,
77
p_round_off,
78
p_currency_id,
79
p_note,
80
p_invoice_status_id,
81
p_created_by,
82
p_invoice_voucher,
83
p_source_warehouse_id,
84
p_destination_warehouse_id,
85
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
86
p_so_no,
87
p_so_date,
88
p_type
89
);
90
RAISE NOTICE 'Invoice header inserted with ID: %', p_id;
91
92
-- Loop through the JSONB array of invoice details
93
FOR detail IN
94
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
95
id uuid, product_id uuid, price numeric, quantity numeric,
96
fees numeric, discount numeric, taxable_amount numeric,
97
sgst_amount numeric, cgst_amount numeric, igst_amount numeric,
98
total_amount numeric, serial_number integer
99
)
100
LOOP
101
-- Insert into invoice details
102
INSERT INTO public.invoice_details (
103
id, invoice_header_id, product_id, price, quantity, fees, discount,
104
taxable_amount, sgst_amount, cgst_amount, igst_amount, total_amount,
105
serial_number, is_deleted
106
) VALUES (
107
detail.id, p_id, detail.product_id, detail.price, detail.quantity, detail.fees,
108
detail.discount, detail.taxable_amount, detail.sgst_amount, detail.cgst_amount,
109
detail.igst_amount, detail.total_amount, detail.serial_number, false
110
);
111
112
RAISE NOTICE 'Invoice detail inserted with ID: %', detail.id;
113
END LOOP;
114
115
-- No explicit COMMIT here
116
RAISE NOTICE 'Transaction completed successfully';
117
118
EXCEPTION
119
-- Catch all other exceptions
120
WHEN OTHERS THEN
121
RAISE EXCEPTION 'An error occurred: %', SQLERRM;
122
123
END;
124
$procedure$
|
|||||
| Procedure | create_apartment_invoice_template | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_apartment_invoice_template(IN p_group_invoice_template_id uuid, IN p_calculation_type integer, IN p_company_id uuid, IN p_account_receivable_id uuid, IN p_sales_account_id uuid, IN p_invoice_date date, IN p_starts_from date, IN p_end_date date, IN p_payment_term integer, IN p_currency_id integer, IN p_fixed_product_id uuid, IN p_bhk_product_id uuid, IN p_sqft_product_id uuid, IN p_fixed_product_rate numeric, IN p_bhk_product_rate numeric, IN p_sqft_product_rate numeric, IN p_note character varying, IN p_invoice_status_id integer, IN p_due_days integer, IN p_billing_cycle text, IN p_created_by uuid, IN p_units json, IN p_active_status boolean, IN p_day_of_week integer DEFAULT NULL::integer, IN p_day_of_month integer DEFAULT NULL::integer, IN p_month_of_year integer DEFAULT NULL::integer, IN p_quarters_of_month integer DEFAULT NULL::integer, IN p_half_year integer DEFAULT NULL::integer, IN p_bi_month integer DEFAULT NULL::integer, IN p_time_of_day interval DEFAULT '00:00:00'::interval)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_unit json;
6
v_unit_id integer;
7
v_bhk numeric;
8
v_sqft_area numeric;
9
v_customer_id uuid;
10
v_total_amount numeric;
11
v_i integer := 0;
12
v_day_of_week integer := COALESCE(p_day_of_week, 0);
13
v_day_of_month integer := COALESCE(p_day_of_month, 0);
14
v_month_of_year integer := COALESCE(p_month_of_year, 0);
15
v_quarters_of_month integer := COALESCE(p_quarters_of_month, 0);
16
v_half_year integer := COALESCE(p_half_year, 0);
17
v_bi_month integer := COALESCE(p_bi_month, 0);
18
v_time_of_day interval := COALESCE(p_time_of_day, '00:00:00'::interval);
19
BEGIN
20
-- Insert into public.group_invoice_templates
21
INSERT INTO public.group_invoice_templates(
22
id, calculation_type_id, account_receivable_id, sales_account_id, invoice_date, starts_from, end_date,
23
grace_period, due_days, fixed_rate, bhk_rate, sqft_rate, invoice_description,
24
billing_cycle, created_by, created_on_utc, company_id, currency_id, bhk_product_id, fixed_product_id, sqft_product_id, active_status)
25
VALUES (
26
p_group_invoice_template_id, p_calculation_type, p_account_receivable_id, p_sales_account_id, p_invoice_date, p_starts_from, p_end_date,
27
p_payment_term, p_due_days, p_fixed_product_rate, p_bhk_product_rate, p_sqft_product_rate, p_note,
28
p_billing_cycle, p_created_by, NOW(), p_company_id, p_currency_id, p_bhk_product_id, p_fixed_product_id, p_sqft_product_id, p_active_status
29
);
30
31
-- Insert into batch_schedules based on billing cycle
32
IF p_billing_cycle = 'Daily' THEN
33
INSERT INTO public.batch_schedules(
34
group_invoice_template_id, billing_cycle, time_of_day, created_on_utc, created_by)
35
VALUES (
36
p_group_invoice_template_id, p_billing_cycle, v_time_of_day, NOW(), p_created_by
37
);
38
ELSIF p_billing_cycle = 'Weekly' THEN
39
INSERT INTO public.batch_schedules(
40
group_invoice_template_id, billing_cycle, day_of_week, time_of_day, created_on_utc, created_by)
41
VALUES (
42
p_group_invoice_template_id, p_billing_cycle, v_day_of_week, v_time_of_day, NOW(), p_created_by
43
);
44
ELSIF p_billing_cycle = 'Monthly' THEN
45
INSERT INTO public.batch_schedules(
46
group_invoice_template_id, billing_cycle, day_of_month, time_of_day, created_on_utc, created_by)
47
VALUES (
48
p_group_invoice_template_id, p_billing_cycle, v_day_of_month, v_time_of_day, NOW(), p_created_by
49
);
50
ELSIF p_billing_cycle = 'Bi-Monthly' THEN
51
INSERT INTO public.batch_schedules(
52
group_invoice_template_id, billing_cycle, bi_month, day_of_month, time_of_day, created_on_utc, created_by)
53
VALUES (
54
p_group_invoice_template_id, p_billing_cycle, v_bi_month, v_day_of_month, v_time_of_day, NOW(), p_created_by
55
);
56
ELSIF p_billing_cycle = 'Quarterly' THEN
57
INSERT INTO public.batch_schedules(
58
group_invoice_template_id, billing_cycle, quarters_of_month, day_of_month, time_of_day, created_on_utc, created_by)
59
VALUES (
60
p_group_invoice_template_id, p_billing_cycle, v_quarters_of_month, v_day_of_month, v_time_of_day, NOW(), p_created_by
61
);
62
ELSIF p_billing_cycle = 'Half-yearly' THEN
63
INSERT INTO public.batch_schedules(
64
group_invoice_template_id, billing_cycle, half_year, day_of_month, time_of_day, created_on_utc, created_by)
65
VALUES (
66
p_group_invoice_template_id, p_billing_cycle, v_half_year, v_day_of_month, v_time_of_day, NOW(), p_created_by
67
);
68
ELSIF p_billing_cycle = 'Yearly' THEN
69
INSERT INTO public.batch_schedules(
70
group_invoice_template_id, billing_cycle, month_of_year, day_of_month, time_of_day, created_on_utc, created_by)
71
VALUES (
72
p_group_invoice_template_id, p_billing_cycle, v_month_of_year, v_day_of_month, v_time_of_day, NOW(), p_created_by
73
);
74
ELSE
75
RAISE EXCEPTION 'Unknown billing cycle: %', p_billing_cycle;
76
END IF;
77
78
-- Loop through units JSON array and insert into apartment_invoice_template_units
79
FOR v_i IN 0 .. json_array_length(p_units) - 1 LOOP
80
v_unit := p_units->v_i;
81
82
v_unit_id := (v_unit->>'id')::integer;
83
v_bhk := (v_unit->>'bhk')::numeric;
84
v_sqft_area := (v_unit->>'sqftArea')::numeric;
85
v_customer_id := (v_unit->>'customerId')::uuid;
86
87
IF v_customer_id IS NULL THEN
88
RAISE EXCEPTION 'Customer ID is null for unit ID %', v_unit_id;
89
END IF;
90
91
INSERT INTO public.group_invoice_template_customers(
92
id, group_invoice_template_id, unit_id, bhk, sqft_area, customer_id, created_on_utc, created_by)
93
VALUES (
94
gen_random_uuid(), p_group_invoice_template_id, v_unit_id, v_bhk, v_sqft_area, v_customer_id, NOW(), p_created_by
95
);
96
END LOOP;
97
EXCEPTION
98
WHEN OTHERS THEN
99
RAISE EXCEPTION 'Error occurred: %', SQLERRM;
100
END;
101
$procedure$
|
|||||
| Procedure | create_draft_invoice_detail | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_draft_invoice_detail(IN p_invoice_header_id uuid, IN p_price numeric, IN p_quantity numeric, IN p_discount numeric, IN p_fees numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_sgst_amount numeric, IN p_taxable_amount numeric, IN p_total_amount numeric, IN p_serial_number integer, IN p_product_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
p_id uuid;
6
BEGIN
7
p_id := gen_random_uuid();
8
9
INSERT INTO public.draft_invoice_details(
10
id,
11
invoice_header_id,
12
price,
13
quantity,
14
cgst_amount,
15
discount,
16
fees,
17
igst_amount,
18
sgst_amount,
19
taxable_amount,
20
total_amount,
21
serial_number,
22
product_id
23
)
24
VALUES (
25
p_id,
26
p_invoice_header_id,
27
p_price,
28
p_quantity,
29
p_cgst_amount,
30
p_discount,
31
p_fees,
32
p_igst_amount,
33
p_sgst_amount,
34
p_taxable_amount,
35
p_total_amount,
36
p_serial_number,
37
p_product_id
38
);
39
END;
40
$procedure$
|
|||||
| Procedure | create_invoice_template | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_invoice_template(IN p_invoiceheader_id uuid, IN p_frequency_cycle text, IN p_month_of_year integer, IN p_day_of_month integer, IN p_day_of_week integer, IN p_time_of_day time without time zone, IN p_starts_from date, IN p_end_date date, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_template_id uuid := gen_random_uuid(); -- Generate a new UUID for the template ID
6
BEGIN
7
-- Insert the new template into the invoice_template table
8
INSERT INTO public.invoice_template (
9
id, invoice_header_id, frequency_cycle, month_of_year, day_of_month,
10
day_of_week, time_of_day, starts_from, end_date, created_on_utc, created_by, is_deleted
11
)
12
VALUES (
13
v_template_id, p_invoiceheader_id, p_frequency_cycle, p_month_of_year, p_day_of_month,
14
p_day_of_week, p_time_of_day, p_starts_from, p_end_date, NOW(), p_created_by, false
15
);
16
17
RAISE NOTICE 'Invoice template created successfully with ID: %', v_template_id;
18
END;
19
$procedure$
|
|||||
| Procedure | create_multiple_invoice_payments | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_multiple_invoice_payments(IN p_invoice_payments jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
payment RECORD;
6
payment_statuses TEXT[] := ARRAY[]::TEXT[];
7
BEGIN
8
-- Loop through each payment in the JSONB array
9
FOR payment IN
10
SELECT * FROM jsonb_to_recordset(p_invoice_payments) AS (
11
invoice_payment_header_id uuid,
12
company_id uuid,
13
customer_id uuid,
14
received_date date,
15
mode_of_payment text,
16
credit_account_id uuid,
17
debit_account_id uuid,
18
reference text,
19
received_amount numeric,
20
grand_total_amount numeric,
21
advance_amount numeric,
22
description text,
23
tds_amount numeric,
24
created_by uuid,
25
invoice_payment_details jsonb
26
)
27
LOOP
28
BEGIN
29
IF NOT EXISTS (
30
SELECT 1
31
FROM public.invoice_payment_headers
32
WHERE reference = payment.reference
33
AND company_id = payment.company_id
34
) THEN
35
CALL public.create_invoice_payment(
36
payment.invoice_payment_header_id,
37
payment.company_id,
38
payment.customer_id,
39
payment.received_date,
40
payment.mode_of_payment,
41
payment.credit_account_id,
42
payment.debit_account_id,
43
payment.reference,
44
payment.received_amount,
45
payment.grand_total_amount,
46
payment.advance_amount,
47
payment.description,
48
payment.tds_amount,
49
payment.created_by,
50
payment.invoice_payment_details
51
);
52
payment_statuses := payment_statuses || format('%s:success', payment.invoice_payment_header_id);
53
ELSE
54
RAISE NOTICE 'Duplicate invoice payment found for reference: %, company_id: %. Skipping.',
55
payment.reference, payment.company_id;
56
payment_statuses := payment_statuses || format('%s:duplicate', payment.invoice_payment_header_id);
57
CONTINUE;
58
END IF;
59
EXCEPTION
60
WHEN OTHERS THEN
61
RAISE NOTICE 'Error processing payment ID: %, Error: %',
62
payment.invoice_payment_header_id, SQLERRM;
63
payment_statuses := payment_statuses || format('%s:failed', payment.invoice_payment_header_id);
64
END;
65
END LOOP;
66
67
-- Final status notice (for app to parse)
68
RAISE NOTICE 'PAYMENT_STATUS:%', to_json(payment_statuses);
69
70
RAISE NOTICE 'All invoice payments processed.';
71
END;
72
$procedure$
|
|||||
| Procedure | delete_apartment_invoice_template | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.delete_apartment_invoice_template(IN p_template_id uuid, IN p_modified_by uuid, IN p_deleted_on_utc timestamp without time zone)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Begin transaction to ensure atomicity
6
PERFORM 1 FROM public.apartment_invoice_templates WHERE id = p_template_id;
7
8
IF FOUND THEN
9
-- Mark all related apartment_invoice_template_units as deleted
10
UPDATE public.apartment_invoice_template_units
11
SET
12
is_deleted = TRUE, -- Use TRUE for boolean
13
modified_by = p_modified_by,
14
deleted_on_utc = p_deleted_on_utc
15
WHERE
16
template_id = p_template_id;
17
-- Mark the template itself as deleted
18
UPDATE public.apartment_invoice_templates
19
SET
20
is_deleted = TRUE, -- Use TRUE for boolean
21
modified_by = p_modified_by,
22
deleted_on_utc = p_deleted_on_utc
23
WHERE
24
id = p_template_id;
25
ELSE
26
-- If the template does not exist, raise an exception
27
RAISE EXCEPTION 'InvoiceTemplate with template_id: % not found.', p_template_id;
28
END IF;
29
END;
30
$procedure$
|
|||||
| Procedure | delete_invoice_data_by_company_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.delete_invoice_data_by_company_id(IN p_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Deleting from invoice_payment_details
6
DELETE FROM public.invoice_payment_details
7
USING public.invoice_payment_headers
8
WHERE invoice_payment_details.invoice_payment_header_id = invoice_payment_headers.id
9
AND invoice_payment_headers.company_id = p_company_id;
10
11
-- Deleting from invoice_payment_headers
12
DELETE FROM public.invoice_payment_headers
13
WHERE company_id = p_company_id;
14
15
-- Deleting from invoice_details
16
DELETE FROM public.invoice_details
17
USING public.invoice_headers
18
WHERE invoice_details.invoice_header_id = invoice_headers.id
19
AND invoice_headers.company_id = p_company_id;
20
21
-- Deleting from invoice_headers
22
DELETE FROM public.invoice_headers
23
WHERE company_id = p_company_id;
24
25
-- Deleting from draft_invoice_details
26
DELETE FROM public.draft_invoice_details
27
USING public.draft_invoice_headers
28
WHERE draft_invoice_details.invoice_header_id = draft_invoice_headers.id
29
AND draft_invoice_headers.company_id = p_company_id;
30
31
-- Deleting from draft_invoice_headers
32
DELETE FROM public.draft_invoice_headers
33
WHERE company_id = p_company_id;
34
35
END;
36
$procedure$
|
|||||
| Procedure | initialize_invoice_header_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_invoice_header_ids(IN p_default_company_id uuid, IN p_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_header_exists BOOLEAN;
6
v_max_id BIGINT;
7
v_last_seq_value BIGINT;
8
BEGIN
9
-- Check if invoice header IDs already exist for the new company
10
SELECT EXISTS (
11
SELECT 1
12
FROM invoice_header_ids
13
WHERE company_id = p_company_id
14
) INTO v_header_exists;
15
16
IF NOT v_header_exists THEN
17
-- Get the current maximum id from the invoice_header_ids table
18
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM invoice_header_ids;
19
20
-- Get the last value generated by the invoice_header_ids_id_seq sequence
21
SELECT last_value INTO v_last_seq_value FROM invoice_header_ids_id_seq;
22
23
-- Check if the sequence needs to be advanced
24
IF v_last_seq_value <= v_max_id THEN
25
-- Advance the sequence to be one greater than the maximum id
26
PERFORM setval('invoice_header_ids_id_seq', v_max_id + 1, false);
27
RAISE NOTICE 'Sequence invoice_header_ids_id_seq advanced to: %', v_max_id + 1;
28
END IF;
29
30
-- Insert new records for the new company
31
INSERT INTO invoice_header_ids (
32
id,
33
company_id,
34
fin_year,
35
invoice_prefix,
36
invoice_length,
37
last_invoice_id
38
)
39
SELECT
40
nextval('invoice_header_ids_id_seq'), -- Use a sequence for generating an integer ID
41
p_company_id, -- Assign the new company ID
42
fin_year, -- Copy financial year
43
invoice_prefix, -- Copy invoice prefix
44
invoice_length, -- Copy invoice length
45
0 -- Copy last invoice ID
46
FROM invoice_header_ids
47
WHERE company_id = p_default_company_id;
48
49
-- Optional: Log the operation
50
RAISE NOTICE 'Invoice header IDs initialized successfully for new company ID: %', p_company_id;
51
ELSE
52
RAISE NOTICE 'Invoice header IDs already exist for company %. Skipping initialization.', p_company_id;
53
END IF;
54
END;
55
$procedure$
|
|||||
| Procedure | get_all_customer_names | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.get_all_customer_names(IN p_company_id uuid, OUT customer_list jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Fetch customer details (Id, Name) for the given CompanyId
6
SELECT jsonb_agg(
7
jsonb_build_object(
8
'Id', c.id,
9
'Name', c.name
10
)
11
)
12
INTO customer_list
13
FROM public.customers c
14
WHERE c.company_id = p_company_id;
15
16
-- If no customers are found, return an empty array
17
IF customer_list IS NULL THEN
18
customer_list := '[]'::jsonb;
19
END IF;
20
END;
21
$procedure$
|
|||||
| Procedure | initialize_group_invoice_header_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_group_invoice_header_ids(IN old_company_id uuid, IN new_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_header_exists BOOLEAN;
6
v_max_id BIGINT;
7
v_last_seq_value BIGINT;
8
BEGIN
9
-- Check if group invoice header IDs already exist for the new company
10
SELECT EXISTS (
11
SELECT 1
12
FROM group_invoice_header_ids
13
WHERE company_id = new_company_id
14
) INTO v_header_exists;
15
16
IF NOT v_header_exists THEN
17
-- Get the current maximum id from the group_invoice_header_ids table
18
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM group_invoice_header_ids;
19
20
-- Get the last value generated by the group_invoice_header_ids_id_seq sequence
21
SELECT last_value INTO v_last_seq_value FROM group_invoice_header_ids_id_seq;
22
23
-- Check if the sequence needs to be advanced
24
IF v_last_seq_value <= v_max_id THEN
25
-- Advance the sequence to be one greater than the maximum id
26
PERFORM setval('group_invoice_header_ids_id_seq', v_max_id + 1, false);
27
RAISE NOTICE 'Sequence group_invoice_header_ids_id_seq advanced to: %', v_max_id + 1;
28
END IF;
29
30
-- Insert new records for the new company
31
INSERT INTO group_invoice_header_ids (
32
id,
33
company_id,
34
fin_year,
35
group_invoice_prefix,
36
group_invoice_length,
37
last_group_invoice_id
38
)
39
SELECT
40
nextval('group_invoice_header_ids_id_seq'), -- Use a sequence for generating an integer ID
41
new_company_id, -- Assign the new company ID
42
fin_year, -- Copy the financial year
43
group_invoice_prefix, -- Copy the group invoice prefix
44
group_invoice_length, -- Copy the group invoice length
45
0 -- Copy the last group invoice ID need to change as default 0
46
FROM group_invoice_header_ids
47
WHERE company_id = old_company_id;
48
49
-- Optional: Log the operation
50
RAISE NOTICE 'Group invoice header IDs initialized successfully for new company ID: %', new_company_id;
51
ELSE
52
RAISE NOTICE 'Group invoice header IDs already exist for company %. Skipping initialization.', new_company_id;
53
END IF;
54
END;
55
$procedure$
|
|||||
| Procedure | hard_delete_org_sales | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.hard_delete_org_sales(IN p_organization_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_company_id uuid;
6
BEGIN
7
FOR v_company_id IN SELECT id FROM companies WHERE organization_id = p_organization_id LOOP
8
DELETE FROM customer_note_headers WHERE company_id = v_company_id;
9
DELETE FROM customer_note_work_flows WHERE company_id = v_company_id;
10
DELETE FROM customers WHERE company_id = v_company_id;
11
12
DELETE FROM draft_invoice_header_ids WHERE company_id = v_company_id;
13
DELETE FROM draft_invoice_headers WHERE company_id = v_company_id;
14
15
DELETE FROM gate_pass_headers WHERE company_id = v_company_id;
16
17
DELETE FROM group_invoice_details WHERE company_id = v_company_id;
18
DELETE FROM group_invoice_header_ids WHERE company_id = v_company_id;
19
DELETE FROM group_invoice_headers WHERE company_id = v_company_id;
20
DELETE FROM group_invoice_templates WHERE company_id = v_company_id;
21
22
DELETE FROM invoice_header_ids WHERE company_id = v_company_id;
23
DELETE FROM invoice_headers WHERE company_id = v_company_id;
24
25
DELETE FROM invoice_payment_header_ids WHERE company_id = v_company_id;
26
DELETE FROM invoice_payment_headers WHERE company_id = v_company_id;
27
28
DELETE FROM invoice_voucher_ids WHERE company_id = v_company_id;
29
DELETE FROM invoice_workflow WHERE company_id = v_company_id;
30
31
DELETE FROM penalty_configs WHERE company_id = v_company_id;
32
DELETE FROM recurring_sales_schedules WHERE company_id = v_company_id;
33
34
DELETE FROM users WHERE company_id = v_company_id;
35
36
DELETE FROM companies WHERE id = v_company_id;
37
38
RAISE NOTICE 'Deleted sales data for company_id %', v_company_id;
39
END LOOP;
40
41
DELETE FROM organizations WHERE id = p_organization_id;
42
43
RAISE NOTICE 'Deleted sales data for organization_id %', p_organization_id;
44
END;
45
$procedure$
|
|||||
| Procedure | hard_delete_organization | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.hard_delete_organization(IN p_org_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Delete records from dependent tables in reverse order of dependencies
6
7
-- Delete customer note workflows for the organization
8
DELETE FROM customer_note_work_flows
9
WHERE company_id IN (
10
SELECT id FROM companies WHERE organization_id = p_org_id
11
);
12
13
-- Delete invoice statuses for the organization
14
DELETE FROM invoice_statuses
15
WHERE created_by IN (
16
SELECT created_by FROM companies WHERE organization_id = p_org_id
17
);
18
19
-- Delete invoice workflows for the organization
20
DELETE FROM invoice_workflow
21
WHERE company_id IN (
22
SELECT id FROM companies WHERE organization_id = p_org_id
23
);
24
25
-- Delete draft invoice header IDs for the organization
26
DELETE FROM draft_invoice_header_ids
27
WHERE company_id IN (
28
SELECT id FROM companies WHERE organization_id = p_org_id
29
);
30
31
-- Delete invoice header IDs for the organization
32
DELETE FROM invoice_header_ids
33
WHERE company_id IN (
34
SELECT id FROM companies WHERE organization_id = p_org_id
35
);
36
37
-- Delete invoice payment header IDs for the organization
38
DELETE FROM invoice_payment_header_ids
39
WHERE company_id IN (
40
SELECT id FROM companies WHERE organization_id = p_org_id
41
);
42
43
-- Delete group invoice header IDs for the organization
44
DELETE FROM group_invoice_header_ids
45
WHERE company_id IN (
46
SELECT id FROM companies WHERE organization_id = p_org_id
47
);
48
49
-- Delete users associated with the organization
50
DELETE FROM users
51
WHERE company_id IN (
52
SELECT id FROM companies WHERE organization_id = p_org_id
53
);
54
55
-- Delete companies associated with the organization
56
DELETE FROM companies
57
WHERE organization_id = p_org_id;
58
59
-- Finally, delete the organization itself
60
DELETE FROM organizations
61
WHERE id = p_org_id;
62
63
-- Optional: Log the operation
64
RAISE NOTICE 'Organization and associated records deleted successfully for organization ID: %', p_org_id;
65
END;
66
$procedure$
|
|||||
| Procedure | initialize_company | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_company(IN p_company_id uuid, IN p_org_id uuid, IN p_company_name text, IN p_is_apartment boolean, IN p_created_by uuid, IN p_default_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_company_exists boolean;
6
BEGIN
7
-- Check if organization already exists by ID
8
SELECT EXISTS (
9
SELECT 1 FROM public.companies WHERE id = p_company_id
10
) INTO v_company_exists;
11
12
IF v_company_exists THEN
13
RAISE NOTICE 'Company with ID % already exists. Skipping initialization.', p_company_id;
14
ELSE
15
-- Insert into companies table
16
INSERT INTO public.companies (
17
id,
18
organization_id,
19
name,
20
is_apartment,
21
created_on_utc,
22
created_by
23
) VALUES (
24
p_company_id, -- New Company ID
25
p_org_id, -- Organization ID
26
p_company_name, -- Organization Name
27
p_is_apartment, -- Is Apartment
28
NOW(), -- Current UTC timestamp
29
p_created_by -- Created by user
30
);
31
32
RAISE NOTICE 'Company Created successfully for company ID: %', p_company_id;
33
END IF;
34
35
-- Call procedures to initialize sales-related configurations
36
CALL public.initialize_invoice_workflow(
37
p_default_company_id, -- Old Company ID
38
p_company_id, -- New Company ID
39
p_created_by -- Created By User ID
40
);
41
42
CALL public.initialize_draft_invoice_header_ids(
43
p_default_company_id, -- Old Company ID
44
p_company_id -- New Company ID
45
);
46
47
CALL public.initialize_invoice_header_ids(
48
p_default_company_id, -- Old Company ID
49
p_company_id -- New Company ID
50
);
51
52
CALL public.initialize_invoice_payment_header_ids(
53
p_default_company_id, -- Old Company ID
54
p_company_id -- New Company ID
55
);
56
57
-- Add call to initialize group invoice header IDs
58
CALL public.initialize_group_invoice_header_ids(
59
p_default_company_id, -- Old Company ID
60
p_company_id -- New Company ID
61
);
62
63
-- Add call to initialize customer note workflows
64
CALL public.initialize_customer_note_work_flows(
65
p_default_company_id, -- Old Company ID
66
p_company_id, -- New Company ID
67
p_created_by -- Created By User ID
68
);
69
70
-- Optional: Log the operation
71
RAISE NOTICE 'Company initialization completed successfully for company ID: %', p_company_id;
72
END;
73
$procedure$
|
|||||
| Procedure | initialize_customer_note_work_flows | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_customer_note_work_flows(IN p_old_company_id uuid, IN p_new_company_id uuid, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_workflow_exists BOOLEAN;
6
v_max_id BIGINT;
7
v_last_seq_value BIGINT;
8
BEGIN
9
-- Check if customer note workflows already exist for the new company
10
SELECT EXISTS (
11
SELECT 1
12
FROM customer_note_work_flows
13
WHERE company_id = p_new_company_id
14
) INTO v_workflow_exists;
15
16
IF NOT v_workflow_exists THEN
17
-- Get the current maximum id from the customer_note_work_flows table
18
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM customer_note_work_flows;
19
20
-- Get the last value generated by the customer_note_work_flows_id_seq sequence
21
SELECT last_value INTO v_last_seq_value FROM customer_note_work_flows_id_seq;
22
23
-- Check if the sequence needs to be advanced
24
IF v_last_seq_value <= v_max_id THEN
25
-- Advance the sequence to be one greater than the maximum id
26
PERFORM setval('customer_note_work_flows_id_seq', v_max_id + 1, false);
27
RAISE NOTICE 'Sequence customer_note_work_flows_id_seq advanced to: %', v_max_id + 1;
28
END IF;
29
30
-- Insert new records into customer_note_work_flows for the new company
31
INSERT INTO customer_note_work_flows (
32
id,
33
company_id,
34
status,
35
next_status,
36
previous_status,
37
is_initial,
38
created_on_utc,
39
created_by,
40
is_deleted
41
)
42
SELECT
43
nextval('customer_note_work_flows_id_seq'), -- Generate new IDs using the sequence
44
p_new_company_id, -- Assign the new company ID
45
status, -- Copy the status
46
next_status, -- Copy the next_status
47
previous_status, -- Copy the previous_status
48
is_initial, -- Copy the is_initial flag
49
NOW(), -- Set current UTC timestamp for created_on_utc
50
p_created_by, -- Assign the created_by user
51
false -- Set is_deleted to false for new records
52
FROM customer_note_work_flows
53
WHERE company_id = p_old_company_id -- Filter records by the old company ID
54
AND is_deleted = false; -- Only copy non-deleted records
55
56
-- Optional: Log the operation
57
RAISE NOTICE 'Customer note workflows have been copied from company % to company % by user %.', p_old_company_id, p_new_company_id, p_created_by;
58
ELSE
59
RAISE NOTICE 'Customer note workflows already exist for company %. Skipping initialization.', p_new_company_id;
60
END IF;
61
END;
62
$procedure$
|
|||||
| Procedure | initialize_invoice_workflow | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_invoice_workflow(IN p_default_company_id uuid, IN p_company_id uuid, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_header_exists BOOLEAN;
6
v_max_id BIGINT;
7
v_last_seq_value BIGINT;
8
BEGIN
9
-- Check if invoice workflow already exists for the new company
10
SELECT EXISTS (
11
SELECT 1
12
FROM invoice_workflow
13
WHERE company_id = p_company_id
14
) INTO v_header_exists;
15
16
IF NOT v_header_exists THEN
17
-- Get the current maximum id from the invoice_workflow table
18
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM public.invoice_workflow;
19
20
-- Get the last value generated by the invoice_workflow_id_seq sequence
21
SELECT last_value INTO v_last_seq_value FROM invoice_workflow_id_seq;
22
23
-- Check if the sequence needs to be advanced
24
IF v_last_seq_value <= v_max_id THEN
25
-- Advance the sequence to be one greater than the maximum id
26
PERFORM setval('invoice_workflow_id_seq', v_max_id + 1, false);
27
RAISE NOTICE 'Sequence invoice_workflow_id_seq advanced to: %', v_max_id + 1;
28
END IF;
29
30
-- Insert new records for the new company
31
INSERT INTO invoice_workflow (
32
id,
33
company_id,
34
status,
35
next_status,
36
previous_status,
37
is_initial,
38
created_on_utc,
39
created_by,
40
approval_level
41
)
42
SELECT
43
nextval('invoice_workflow_id_seq'),
44
p_company_id,
45
status,
46
next_status,
47
previous_status,
48
is_initial,
49
NOW(),
50
p_created_by,
51
approval_level -- The approval_level from the default company
52
FROM invoice_workflow
53
WHERE company_id = p_default_company_id;
54
55
-- Optional: Log the operation
56
RAISE NOTICE 'Invoice workflow records have been copied from company % to company % by user %.', p_default_company_id, p_company_id, p_created_by;
57
ELSE
58
RAISE NOTICE 'Invoice workflow records already exist for company %. Skipping initialization.', p_company_id;
59
END IF;
60
END;
61
$procedure$
|
|||||
| Procedure | log_invoice_approval | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.log_invoice_approval(IN p_invoice_id uuid, IN p_next_status integer, IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
APPROVED_STATUS CONSTANT INTEGER := 3;
6
FIRST_LEVEL_APPROVAL CONSTANT INTEGER := 8;
7
SECOND_LEVEL_APPROVAL CONSTANT INTEGER := 9;
8
BEGIN
9
IF p_next_status = APPROVED_STATUS OR
10
p_next_status = FIRST_LEVEL_APPROVAL OR
11
p_next_status = SECOND_LEVEL_APPROVAL THEN
12
13
INSERT INTO public.invoice_approval_logs(
14
invoice_id,
15
status_id,
16
approved_by,
17
approved_on,
18
"comment",
19
created_on_utc,
20
created_by
21
)
22
VALUES(
23
p_invoice_id,
24
p_next_status,
25
p_modified_by,
26
NOW(),
27
CASE
28
WHEN p_next_status = FIRST_LEVEL_APPROVAL THEN 'Level 1 Approval'
29
WHEN p_next_status = SECOND_LEVEL_APPROVAL THEN 'Level 2 Approval'
30
WHEN p_next_status = APPROVED_STATUS THEN 'Final Approval'
31
ELSE 'Status updated'
32
END,
33
NOW(),
34
p_modified_by
35
);
36
RAISE NOTICE 'Approval logged for invoice %, status %', p_invoice_id, p_next_status;
37
END IF;
38
END;
39
$procedure$
|
|||||
| Procedure | run_batches | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.run_batches()
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
schedule_record RECORD;
6
v_unit RECORD;
7
v_group_invoice_id UUID;
8
v_invoice_header_id UUID;
9
v_total_amount NUMERIC;
10
v_status TEXT;
11
v_error_message TEXT;
12
v_total_count INTEGER;
13
v_success_count INTEGER;
14
v_calculation_type INTEGER;
15
v_company_id UUID;
16
v_sales_account_id UUID;
17
v_account_receivable_id UUID;
18
v_invoice_date DATE;
19
v_starts_from DATE;
20
v_end_date DATE;
21
v_due_date DATE;
22
v_payment_term INTEGER;
23
v_currency_id INTEGER;
24
v_fixed_product_id UUID;
25
v_bhk_product_id UUID;
26
v_sqft_product_id UUID;
27
v_fixed_product_rate NUMERIC;
28
v_bhk_product_rate NUMERIC;
29
v_sqft_product_rate NUMERIC;
30
v_note TEXT;
31
v_invoice_status_id INTEGER;
32
v_due_days INTEGER;
33
v_billing_cycle TEXT;
34
v_active_status boolean;
35
-- Variables taken from batch_schedules table for the particular group_invoice_template_id
36
v_half_year INTEGER;
37
v_quarters_of_month INTEGER;
38
v_bi_month INTEGER;
39
v_day_of_week INTEGER; -- From batch_schedules table
40
v_day_of_month INTEGER; -- From batch_schedules table
41
v_month_of_year INTEGER; -- From batch_schedules table
42
v_time_of_day INTERVAL := '15:23:00'; -- Set time_of_day to 02:05 AM
43
v_created_by UUID;
44
v_group_invoice_number character varying;
45
--v_fin_year INTEGER;
46
BEGIN
47
-- Loop through all non-deleted schedules that should run today
48
FOR schedule_record IN
49
SELECT *
50
FROM batch_schedules
51
WHERE is_deleted = FALSE
52
AND group_invoice_template_id IN (
53
SELECT id
54
FROM public.group_invoice_templates
55
WHERE is_deleted = FALSE
56
AND (
57
-- Only process templates where start date <= current date and end date is either null or in the future
58
starts_from <= CURRENT_DATE
59
AND (end_date IS NULL OR end_date >= CURRENT_DATE)
60
)
61
)
62
-- Check if the time of day is 02:05 AM as per the requirement
63
AND last_executed_at IS DISTINCT FROM CURRENT_DATE::timestamp
64
LOOP
65
-- Now you can access end_date as it has been included in the query
66
v_half_year := schedule_record.half_year;
67
v_quarters_of_month := schedule_record.quarters_of_month;
68
v_bi_month := schedule_record.bi_month;
69
v_day_of_week := schedule_record.day_of_week;
70
v_day_of_month := schedule_record.day_of_month;
71
v_month_of_year := schedule_record.month_of_year;
72
73
-- Process based on billing cycle and check if current time matches the set time
74
IF (
75
-- Daily schedules
76
schedule_record.billing_cycle = 'Daily'
77
-- Weekly schedules running on the current day of the week
78
OR (schedule_record.billing_cycle = 'Weekly' AND v_day_of_week = EXTRACT(DOW FROM CURRENT_DATE))
79
-- Monthly schedules running on the current day of the month
80
OR (schedule_record.billing_cycle = 'Monthly' AND v_day_of_month = EXTRACT(DAY FROM CURRENT_DATE) AND CURRENT_DATE <= schedule_record.end_date)
81
-- Bi-Monthly schedules: run every two months
82
OR (schedule_record.billing_cycle = 'Bi-Monthly' AND v_bi_month = CEIL(EXTRACT(MONTH FROM CURRENT_DATE) / 2) AND CURRENT_DATE <= schedule_record.end_date)
83
-- Quarterly schedules: run every three months
84
OR (schedule_record.billing_cycle = 'Quarterly' AND v_quarters_of_month = CEIL(EXTRACT(MONTH FROM CURRENT_DATE) / 3) AND CURRENT_DATE <= schedule_record.end_date)
85
-- Half-yearly schedules: run every six months
86
OR (schedule_record.billing_cycle = 'Half-yearly' AND v_half_year = CASE WHEN EXTRACT(MONTH FROM CURRENT_DATE) <= 6 THEN 1 ELSE 2 END AND CURRENT_DATE <= schedule_record.end_date)
87
-- Yearly schedules: run based on month of the year
88
OR (schedule_record.billing_cycle = 'Yearly' AND v_month_of_year = EXTRACT(MONTH FROM CURRENT_DATE) AND CURRENT_DATE <= schedule_record.end_date)
89
)
90
-- Check if the time of day is 02:05 AM or later
91
AND CURRENT_TIME >= v_time_of_day THEN
92
-- Begin processing the batch for this template
93
v_group_invoice_id := gen_random_uuid();
94
v_total_count := 0;
95
v_success_count := 0;
96
v_status := 'pending';
97
v_error_message := '';
98
99
-- Get template details
100
SELECT calculation_type_id, company_id, sales_account_id, invoice_date, starts_from, grace_period, due_days,
101
fixed_rate, bhk_rate, sqft_rate, invoice_description, billing_cycle, account_receivable_id,
102
currency_id, bhk_product_id, fixed_product_id, sqft_product_id, end_date, created_by, active_status
103
INTO v_calculation_type, v_company_id, v_sales_account_id, v_invoice_date, v_starts_from, v_payment_term,
104
v_due_days, v_fixed_product_rate, v_bhk_product_rate, v_sqft_product_rate, v_note, v_billing_cycle,
105
v_account_receivable_id, v_currency_id, v_bhk_product_id, v_fixed_product_id, v_sqft_product_id, v_end_date,
106
v_created_by, v_active_status
107
FROM public.group_invoice_templates
108
WHERE id = schedule_record.group_invoice_template_id;
109
110
-- Skip processing if the template is not active
111
IF NOT v_active_status THEN
112
RAISE NOTICE 'Skipping template with ID: %, as active_status is FALSE', schedule_record.group_invoice_template_id;
113
CONTINUE; -- Move to the next schedule_record
114
END IF;
115
116
-- Calculate the due date
117
v_due_date := v_invoice_date + v_due_days;
118
119
-- Skip processing if the due date is before the starts_from date
120
IF v_starts_from IS NOT NULL AND v_due_date < v_starts_from THEN
121
CONTINUE;
122
END IF;
123
124
-- Skip processing if the template has ended
125
IF v_end_date IS NOT NULL AND v_end_date < CURRENT_DATE THEN
126
CONTINUE;
127
END IF;
128
129
v_group_invoice_number := get_new_group_invoice_number(v_company_id);--, v_fin_year);
130
131
RAISE NOTICE 'Value: %', v_group_invoice_number;
132
133
-- Insert into group_inovice_headers
134
INSERT INTO public.group_inovice_headers (
135
id, group_invoice_template_id, execution_date, success_count, created_on_utc, created_by, total_count, error_message, company_id, group_invoice_number
136
)
137
VALUES (
138
v_group_invoice_id, schedule_record.group_invoice_template_id, NOW(), v_success_count, NOW(), 'SYSTEM', v_total_count, v_error_message, v_company_id, v_group_invoice_number
139
);
140
141
-- Select Status from the invoice workflow
142
SELECT MIN(status)
143
INTO v_invoice_status_id
144
FROM public.invoice_workflow
145
WHERE company_id = v_company_id
146
AND is_deleted = FALSE;
147
148
-- Loop through units associated with the template
149
FOR v_unit IN
150
SELECT unit_id, bhk, sqft_area, customer_id
151
FROM public.group_invoice_template_customers
152
WHERE group_invoice_template_id = schedule_record.group_invoice_template_id
153
LOOP
154
BEGIN
155
v_total_count := v_total_count + 1;
156
v_invoice_header_id := gen_random_uuid();
157
v_total_amount := 0;
158
159
-- Calculate the total amount based on the calculation type
160
CASE v_calculation_type
161
WHEN 1 THEN -- Fixed method
162
v_total_amount := v_fixed_product_rate;
163
WHEN 2 THEN -- BHK
164
v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
165
WHEN 3 THEN -- SFT
166
v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
167
WHEN 4 THEN -- Fixed + BHK
168
v_total_amount := v_fixed_product_rate + (v_bhk_product_rate * (v_unit.bhk)::NUMERIC);
169
WHEN 5 THEN -- Fixed + SFT
170
v_total_amount := v_fixed_product_rate + (v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC);
171
END CASE;
172
173
IF v_calculation_type = 1 THEN --FIxed
174
CALL public.create_invoice_detail(v_invoice_header_id, v_fixed_product_rate, 1, 0, 0, 0, 0, 0, v_fixed_product_rate, v_fixed_product_rate, 1, v_fixed_product_id);
175
ELSIF v_calculation_type = 2 THEN -- BHK
176
v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
177
CALL public.create_invoice_detail(v_invoice_header_id, v_bhk_product_rate, (v_unit.bhk)::NUMERIC, 0, 0, 0, 0, 0, v_total_amount, v_total_amount, 1, v_bhk_product_id);
178
ELSIF v_calculation_type = 3 THEN -- SFT
179
v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
180
CALL public.create_invoice_detail(v_invoice_header_id, v_sqft_product_rate, (v_unit.sqft_area)::NUMERIC, 0, 0, 0, 0, 0, v_total_amount, v_total_amount, 1, v_sqft_product_id);
181
ELSIF v_calculation_type = 4 THEN -- Fixed + BHK
182
CALL public.create_invoice_detail(v_invoice_header_id, v_fixed_product_rate, 1, 0, 0, 0, 0, 0, v_fixed_product_rate, v_fixed_product_rate, 1, v_fixed_product_id);
183
v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
184
CALL public.create_invoice_detail(v_invoice_header_id, v_bhk_product_rate, (v_unit.bhk)::NUMERIC, 0, 0, 0, 0, 0, v_total_amount, v_total_amount, 2, v_bhk_product_id);
185
ELSIF v_calculation_type = 5 THEN -- Fixed + SFT
186
CALL public.create_invoice_detail(v_invoice_header_id, v_fixed_product_rate, 1, 0, 0, 0, 0, 0, v_fixed_product_rate, v_fixed_product_rate, 1, v_fixed_product_id);
187
v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
188
CALL public.create_invoice_detail(v_invoice_header_id, v_sqft_product_rate, (v_unit.sqft_area)::NUMERIC, 0, 0, 0, 0, 0, v_total_amount, v_total_amount, 2, v_sqft_product_id);
189
END IF;
190
191
v_status := 'success';
192
v_success_count := v_success_count + 1;
193
194
-- Insert into group_inovice_details
195
INSERT INTO public.group_inovice_details(
196
id, group_invoice_id, unit_id, invoice_header_id, status, posted_on, error_message, created_on_utc, created_by)
197
VALUES (
198
gen_random_uuid(), v_group_invoice_id, v_unit.unit_id, v_invoice_header_id, v_status, NOW(),v_error_message, NOW(), 'SYSTEM'
199
);
200
201
EXCEPTION
202
WHEN OTHERS THEN
203
-- Handle any errors during invoice posting
204
v_status := 'failed';
205
v_error_message := COALESCE(SQLERRM, 'Unknown error');
206
207
-- Insert into group_inovice_details with failed status
208
INSERT INTO public.group_inovice_details(
209
id, group_invoice_id, unit_id, status, invoice_header_id, error_message, posted_on, created_on_utc, created_by)
210
VALUES (
211
gen_random_uuid(), v_group_invoice_id, v_unit.unit_id, v_status, v_invoice_header_id, COALESCE(v_error_message, 'Unknown error'), NOW(), NOW(), 'SYSTEM'
212
);
213
END;
214
END LOOP;
215
216
-- Update execution status based on success/failure
217
IF v_success_count = v_total_count THEN
218
v_status := 'success';
219
ELSIF v_success_count > 0 THEN
220
v_status := 'partially failed';
221
ELSE
222
v_status := 'failed';
223
END IF;
224
225
-- Update execution record with final status and counts
226
UPDATE public.group_inovice_headers
227
SET success_count = v_success_count,
228
total_count = v_total_count,
229
modified_on_utc = NOW(),
230
modified_by = v_created_by,
231
error_message = COALESCE(v_error_message, '')
232
WHERE id = v_group_invoice_id;
233
234
-- Update last executed timestamp for the schedule
235
UPDATE batch_schedules
236
SET last_executed_at = NOW()
237
WHERE id = schedule_record.id;
238
END IF;
239
END LOOP;
240
END;
241
$procedure$
|
|||||
| Procedure | run_grouped_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.run_grouped_invoices(IN p_template_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
schedule_record RECORD;
6
v_unit RECORD;
7
v_group_invoice_header_id UUID;
8
v_invoice_header_id UUID;
9
v_total_amount NUMERIC;
10
v_status TEXT;
11
v_error_message TEXT;
12
v_total_count INTEGER;
13
v_success_count INTEGER;
14
v_calculation_type INTEGER;
15
v_company_id UUID;
16
v_sales_account_id UUID;
17
v_account_receivable_id UUID;
18
v_invoice_date DATE;
19
v_starts_from DATE;
20
v_end_date DATE;
21
v_due_date DATE;
22
v_payment_term INTEGER;
23
v_currency_id INTEGER;
24
v_fixed_product_id UUID;
25
v_bhk_product_id UUID;
26
v_sqft_product_id UUID;
27
v_fixed_product_rate NUMERIC;
28
v_bhk_product_rate NUMERIC;
29
v_sqft_product_rate NUMERIC;
30
v_note TEXT;
31
v_invoice_status_id INTEGER;
32
v_due_days INTEGER;
33
v_billing_cycle TEXT;
34
v_active_status BOOLEAN;
35
v_day_of_week INTEGER;
36
v_day_of_month INTEGER;
37
v_month_of_year INTEGER;
38
v_time_of_day TIME;
39
v_created_by UUID;
40
v_group_invoice_number VARCHAR;
41
v_serial_number INTEGER := 1;
42
v_product_rate NUMERIC;
43
v_quantity NUMERIC;
44
v_product_id UUID;
45
v_template_exists BOOL;
46
v_template_is_deleted BOOL;
47
BEGIN
48
RAISE NOTICE '[START] run_grouped_invoices(template_id=%)', p_template_id;
49
50
-- Extra visibility on template state
51
SELECT EXISTS (SELECT 1 FROM public.group_invoice_templates WHERE id = p_template_id) AS exists,
52
COALESCE( (SELECT is_deleted FROM public.group_invoice_templates WHERE id = p_template_id LIMIT 1), NULL) AS is_deleted
53
INTO v_template_exists, v_template_is_deleted;
54
55
RAISE NOTICE '[CHECK] template exists? %, is_deleted? %', v_template_exists, v_template_is_deleted;
56
57
IF NOT EXISTS (
58
SELECT 1 FROM public.group_invoice_templates
59
WHERE id = p_template_id AND is_deleted = FALSE
60
) THEN
61
RAISE EXCEPTION 'Invalid template_id: % (exists=%, is_deleted=%)', p_template_id, v_template_exists, v_template_is_deleted;
62
END IF;
63
64
v_time_of_day := CURRENT_TIME;
65
RAISE NOTICE '[INFO] current_time=%', v_time_of_day;
66
67
FOR schedule_record IN
68
SELECT *
69
FROM batch_schedules bs
70
WHERE bs.group_invoice_template_id = p_template_id
71
AND bs.is_deleted = FALSE
72
AND bs.last_executed_at IS DISTINCT FROM CURRENT_DATE::timestamp
73
LOOP
74
RAISE NOTICE '[SCHEDULE] processing schedule_id=% last_executed_at=%', schedule_record.id, schedule_record.last_executed_at;
75
76
SELECT calculation_type_id, company_id, sales_account_id, invoice_date, starts_from, grace_period, due_days,
77
fixed_rate, bhk_rate, sqft_rate, invoice_description, billing_cycle, account_receivable_id,
78
currency_id, bhk_product_id, fixed_product_id, sqft_product_id, end_date, created_by, active_status
79
INTO v_calculation_type, v_company_id, v_sales_account_id, v_invoice_date, v_starts_from, v_payment_term,
80
v_due_days, v_fixed_product_rate, v_bhk_product_rate, v_sqft_product_rate, v_note, v_billing_cycle,
81
v_account_receivable_id, v_currency_id, v_bhk_product_id, v_fixed_product_id, v_sqft_product_id, v_end_date,
82
v_created_by, v_active_status
83
FROM public.group_invoice_templates git
84
WHERE git.id = p_template_id;
85
86
RAISE NOTICE '[TEMPLATE] company_id=% calc_type=% inv_date=% starts_from=% due_days=% end_date=% active=%',
87
v_company_id, v_calculation_type, v_invoice_date, v_starts_from, v_due_days, v_end_date, v_active_status;
88
89
-- Force System user
90
SELECT id INTO v_created_by
91
FROM users
92
WHERE first_name = 'System'
93
LIMIT 1;
94
95
RAISE NOTICE '[USER] created_by (System) id=%', v_created_by;
96
97
IF v_created_by IS NULL THEN
98
RAISE EXCEPTION 'System user not found in the users table';
99
END IF;
100
101
IF NOT v_active_status THEN
102
RAISE NOTICE '[SKIP] template inactive';
103
CONTINUE;
104
END IF;
105
106
v_due_date := v_invoice_date + v_due_days;
107
RAISE NOTICE '[DATES] invoice_date=% due_days=% due_date=%', v_invoice_date, v_due_days, v_due_date;
108
109
IF v_starts_from IS NOT NULL AND v_due_date < v_starts_from THEN
110
RAISE NOTICE '[SKIP] due_date % < starts_from %', v_due_date, v_starts_from;
111
CONTINUE;
112
END IF;
113
114
IF v_end_date IS NOT NULL AND v_end_date < CURRENT_DATE THEN
115
RAISE NOTICE '[SKIP] end_date % < today %', v_end_date, CURRENT_DATE;
116
CONTINUE;
117
END IF;
118
119
v_group_invoice_header_id := gen_random_uuid();
120
v_total_count := 0;
121
v_success_count := 0;
122
v_status := 'pending';
123
v_error_message := '';
124
125
v_group_invoice_number := get_new_group_invoice_number(v_company_id, v_invoice_date);
126
RAISE NOTICE '[GROUP] new group_invoice_header_id=% number=%', v_group_invoice_header_id, v_group_invoice_number;
127
128
INSERT INTO public.group_invoice_headers (
129
id, group_invoice_template_id, execution_date, success_count,
130
created_on_utc, created_by, total_count, error_message, company_id, group_invoice_number
131
)
132
VALUES (
133
v_group_invoice_header_id, p_template_id, NOW(), v_success_count,
134
NOW(), v_created_by, v_total_count, v_error_message, v_company_id, v_group_invoice_number
135
);
136
137
SELECT status
138
INTO v_invoice_status_id
139
FROM public.invoice_workflow iw
140
WHERE iw.company_id = v_company_id
141
AND iw.is_deleted = FALSE
142
AND iw.status = 3
143
LIMIT 1;
144
145
RAISE NOTICE '[WORKFLOW] invoice_status_id=%', v_invoice_status_id;
146
147
FOR v_unit IN
148
SELECT unit_id, bhk, sqft_area, customer_id
149
FROM public.group_invoice_template_customers gitc
150
WHERE gitc.group_invoice_template_id = p_template_id
151
LOOP
152
BEGIN
153
v_total_count := v_total_count + 1;
154
v_invoice_header_id := gen_random_uuid();
155
v_total_amount := 0;
156
v_serial_number := 1;
157
158
RAISE NOTICE ' [UNIT] unit_id=% customer_id=% bhk=% sqft=%', v_unit.unit_id, v_unit.customer_id, v_unit.bhk, v_unit.sqft_area;
159
160
-- Compute total first
161
CASE v_calculation_type
162
WHEN 1 THEN v_total_amount := v_fixed_product_rate;
163
WHEN 2 THEN v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
164
WHEN 3 THEN v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
165
WHEN 4 THEN v_total_amount := v_fixed_product_rate + (v_bhk_product_rate * (v_unit.bhk)::NUMERIC);
166
WHEN 5 THEN v_total_amount := v_fixed_product_rate + (v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC);
167
ELSE RAISE EXCEPTION 'Invalid calculation type: %', v_calculation_type;
168
END CASE;
169
170
RAISE NOTICE ' [AMOUNT] calc_type=% total_amount=%', v_calculation_type, v_total_amount;
171
172
-- HEADER first (fixes FK)
173
CALL public.create_invoice_header(
174
v_invoice_header_id,
175
v_company_id,
176
v_unit.customer_id,
177
v_account_receivable_id,
178
v_sales_account_id,
179
v_invoice_date,
180
v_due_date,
181
v_payment_term,
182
v_total_amount,
183
0,0,0,
184
v_total_amount,
185
0,0,0,
186
v_currency_id,
187
v_note,
188
v_invoice_status_id,
189
v_created_by,
190
'IVAP000',
191
'00000000-0000-0000-0000-000000000000',
192
'00000000-0000-0000-0000-000000000000',
193
'AP',
194
v_invoice_date,
195
3
196
);
197
RAISE NOTICE ' [HEADER CREATED] invoice_header_id=%', v_invoice_header_id;
198
199
-- DETAILS now
200
CASE v_calculation_type
201
WHEN 1 THEN
202
v_product_rate := v_fixed_product_rate; v_quantity := 1; v_product_id := v_fixed_product_id;
203
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
204
RAISE NOTICE ' [DETAIL ADDED] fixed rate line';
205
206
WHEN 2 THEN
207
v_product_rate := v_bhk_product_rate * (v_unit.bhk)::NUMERIC; v_quantity := 1; v_product_id := v_bhk_product_id;
208
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
209
RAISE NOTICE ' [DETAIL ADDED] bhk line';
210
211
WHEN 3 THEN
212
v_product_rate := v_sqft_product_rate; v_quantity := v_unit.sqft_area; v_product_id := v_sqft_product_id;
213
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate*v_quantity, v_product_rate*v_quantity, v_serial_number, v_product_id);
214
RAISE NOTICE ' [DETAIL ADDED] sqft line';
215
216
WHEN 4 THEN
217
-- fixed
218
v_product_rate := v_fixed_product_rate; v_quantity := 1; v_product_id := v_fixed_product_id;
219
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
220
v_serial_number := v_serial_number + 1;
221
-- bhk
222
v_product_rate := v_bhk_product_rate * (v_unit.bhk)::NUMERIC; v_quantity := 1; v_product_id := v_bhk_product_id;
223
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
224
RAISE NOTICE ' [DETAIL ADDED] fixed + bhk lines';
225
226
WHEN 5 THEN
227
-- fixed
228
v_product_rate := v_fixed_product_rate; v_quantity := 1; v_product_id := v_fixed_product_id;
229
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
230
v_serial_number := v_serial_number + 1;
231
-- sqft
232
v_product_rate := v_sqft_product_rate; v_quantity := v_unit.sqft_area; v_product_id := v_sqft_product_id;
233
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate*v_quantity, v_product_rate*v_quantity, v_serial_number, v_product_id);
234
RAISE NOTICE ' [DETAIL ADDED] fixed + sqft lines';
235
END CASE;
236
237
v_status := 'success';
238
v_success_count := v_success_count + 1;
239
240
INSERT INTO public.group_invoice_details(
241
id, group_invoice_header_id, unit_id, invoice_header_id, status, posted_on,
242
error_message, created_on_utc, created_by, company_id
243
)
244
VALUES (
245
gen_random_uuid(), v_group_invoice_header_id, v_unit.unit_id, v_invoice_header_id, v_status, NOW(),
246
'', NOW(), v_created_by, v_company_id
247
);
248
249
RAISE NOTICE ' [UNIT DONE] unit_id=% status=%', v_unit.unit_id, v_status;
250
251
EXCEPTION
252
WHEN OTHERS THEN
253
v_status := 'failed';
254
v_error_message := COALESCE(SQLERRM, 'Unknown error');
255
256
RAISE NOTICE ' [UNIT ERROR] unit_id=% sqlstate=% errmsg=%', v_unit.unit_id, SQLSTATE, v_error_message;
257
258
INSERT INTO public.group_invoice_details(
259
id, group_invoice_header_id, unit_id, status, invoice_header_id, error_message, posted_on,
260
created_on_utc, created_by, company_id
261
)
262
VALUES (
263
gen_random_uuid(), v_group_invoice_header_id, v_unit.unit_id, v_status, v_invoice_header_id,
264
v_error_message, NOW(), NOW(), v_created_by, v_company_id
265
);
266
END;
267
END LOOP;
268
269
UPDATE public.group_invoice_headers
270
SET success_count = v_success_count,
271
total_count = v_total_count,
272
modified_on_utc = NOW(),
273
modified_by = v_created_by,
274
error_message = COALESCE(v_error_message, '')
275
WHERE id = v_group_invoice_header_id;
276
277
RAISE NOTICE '[GROUP DONE] group_header_id=% success=% total=%', v_group_invoice_header_id, v_success_count, v_total_count;
278
279
UPDATE batch_schedules
280
SET last_executed_at = NOW()
281
WHERE id = schedule_record.id;
282
283
RAISE NOTICE '[SCHEDULE DONE] schedule_id=% last_executed_at updated', schedule_record.id;
284
END LOOP;
285
286
RAISE NOTICE '[END] run_grouped_invoices(template_id=%)', p_template_id;
287
END;
288
$procedure$
|
|||||
| Procedure | run_sales_invoice_schedule | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.run_sales_invoice_schedule(IN p_execution_date date DEFAULT CURRENT_DATE)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
r_template RECORD;
6
r_detail RECORD;
7
r_hdr RECORD;
8
r_det RECORD;
9
10
v_expected_execution_date DATE;
11
v_new_invoice_id UUID;
12
v_triggered_by UUID; -- <- now resolved from users table
13
v_log_id UUID;
14
v_freq text;
15
v_now_time time := NOW()::time;
16
v_lock_ok boolean;
17
BEGIN
18
SELECT u.id
19
INTO v_triggered_by
20
FROM public.users u
21
WHERE u.is_deleted = false
22
AND lower(u.first_name) = 'syastem'
23
ORDER BY u.created_on_utc NULLS LAST, u.id
24
LIMIT 1;
25
26
IF v_triggered_by IS NULL THEN
27
RAISE NOTICE 'No user found with first_name="Syastem"; using fallback id %', v_triggered_by;
28
END IF;
29
FOR r_template IN
30
SELECT s.*
31
FROM public.recurring_sales_schedules s
32
WHERE s.is_deleted = false
33
AND s.schedule_status = 'active'
34
AND s.starts_from <= p_execution_date
35
AND (s.end_date IS NULL OR s.end_date >= p_execution_date)
36
ORDER BY s.id
37
LOOP
38
-- Load one detail row (adapt if you support multiple rows per schedule)
39
SELECT d.*
40
INTO r_detail
41
FROM public.recurrence_schedule_details d
42
WHERE d.schedule_id = r_template.id
43
AND d.is_deleted = false
44
LIMIT 1;
45
46
IF NOT FOUND THEN
47
RAISE NOTICE 'No recurrence detail found for schedule %, skipping.', r_template.id;
48
CONTINUE;
49
END IF;
50
51
v_freq := lower(r_detail.frequency_cycle);
52
v_expected_execution_date := NULL;
53
54
IF v_freq = 'Daily' THEN
55
v_expected_execution_date := p_execution_date;
56
57
ELSIF v_freq = 'Weekly' THEN
58
IF r_detail.day_of_week IS NULL THEN
59
RAISE NOTICE 'Schedule % weekly but day_of_week is NULL; skipping.', r_template.id;
60
CONTINUE;
61
END IF;
62
IF r_detail.day_of_week = TO_CHAR(p_execution_date, 'ID')::int THEN
63
v_expected_execution_date := p_execution_date;
64
END IF;
65
66
ELSIF v_freq = 'Monthly' THEN
67
IF r_detail.day_of_month IS NULL THEN
68
RAISE NOTICE 'Schedule % monthly but day_of_month is NULL; skipping.', r_template.id;
69
CONTINUE;
70
END IF;
71
IF r_detail.day_of_month = EXTRACT(DAY FROM p_execution_date)::int THEN
72
v_expected_execution_date := p_execution_date;
73
END IF;
74
75
ELSIF v_freq = 'Yearly' THEN
76
IF r_detail.month_of_year IS NULL OR r_detail.day_of_month IS NULL THEN
77
RAISE NOTICE 'Schedule % yearly but month/day not set; skipping.', r_template.id;
78
CONTINUE;
79
END IF;
80
IF r_detail.month_of_year = EXTRACT(MONTH FROM p_execution_date)::int
81
AND r_detail.day_of_month = EXTRACT(DAY FROM p_execution_date)::int THEN
82
v_expected_execution_date := p_execution_date;
83
END IF;
84
85
ELSE
86
RAISE NOTICE 'Invalid frequency_cycle "%" on schedule %, skipping.', r_detail.frequency_cycle, r_template.id;
87
CONTINUE;
88
END IF;
89
90
IF v_expected_execution_date IS NULL THEN
91
RAISE NOTICE 'Schedule % not aligned for %, skipping.', r_template.id, p_execution_date;
92
CONTINUE;
93
END IF;
94
95
-- Skip if already executed for this date (defense #1)
96
IF EXISTS (
97
SELECT 1 FROM public.schedule_execution_logs l
98
WHERE l.schedule_id = r_template.id
99
AND l.execution_date = v_expected_execution_date
100
AND l.is_deleted = false
101
) THEN
102
RAISE NOTICE 'Schedule % already executed for %, skipping.', r_template.id, v_expected_execution_date;
103
CONTINUE;
104
END IF;
105
106
-- Per-schedule advisory lock (defense #2)
107
v_lock_ok := pg_try_advisory_lock(hashtextextended(r_template.id::text, 0));
108
IF NOT v_lock_ok THEN
109
RAISE NOTICE 'Schedule % locked by another worker, skipping this run.', r_template.id;
110
CONTINUE;
111
END IF;
112
113
v_log_id := gen_random_uuid();
114
115
BEGIN
116
-- Re-check within the lock (defense #3)
117
IF EXISTS (
118
SELECT 1 FROM public.schedule_execution_logs l
119
WHERE l.schedule_id = r_template.id
120
AND l.execution_date = v_expected_execution_date
121
AND l.is_deleted = false
122
) THEN
123
RAISE NOTICE 'Schedule % already executed for % (post-lock), skipping.', r_template.id, v_expected_execution_date;
124
PERFORM pg_advisory_unlock(hashtextextended(r_template.id::text, 0));
125
CONTINUE;
126
END IF;
127
128
-- Load template header (prefer live, then draft)
129
SELECT * INTO r_hdr
130
FROM public.invoice_headers
131
WHERE id = r_template.invoice_header_id
132
AND is_deleted = false;
133
134
IF NOT FOUND THEN
135
SELECT * INTO r_hdr
136
FROM public.draft_invoice_headers
137
WHERE id = r_template.invoice_header_id
138
AND is_deleted = false;
139
END IF;
140
141
IF NOT FOUND THEN
142
INSERT INTO public.schedule_execution_logs (
143
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
144
)
145
VALUES (v_log_id, r_template.id, v_expected_execution_date, NOW(), 'Failure',
146
'Invoice header not found for template; skipping.', v_triggered_by, false);
147
RAISE NOTICE 'Invoice header not found for schedule %, skipping.', r_template.id;
148
PERFORM pg_advisory_unlock(hashtextextended(r_template.id::text, 0));
149
CONTINUE;
150
END IF;
151
152
-- Create new invoice (draft or committed)
153
v_new_invoice_id := gen_random_uuid();
154
155
IF r_template.default_status = 3 THEN
156
CALL public.create_invoice_header(
157
v_new_invoice_id,
158
r_hdr.company_id,
159
r_hdr.customer_id,
160
r_hdr.debit_account_id,
161
r_hdr.credit_account_id,
162
p_execution_date,
163
r_hdr.due_date,
164
r_hdr.payment_term,
165
r_hdr.taxable_amount,
166
r_hdr.cgst_amount,
167
r_hdr.sgst_amount,
168
r_hdr.igst_amount,
169
r_hdr.total_amount,
170
r_hdr.discount,
171
r_hdr.fees,
172
r_hdr.round_off,
173
r_hdr.currency_id,
174
r_hdr.note,
175
3,
176
v_triggered_by,
177
r_hdr.invoice_voucher,
178
r_hdr.source_warehouse_id,
179
r_hdr.destination_warehouse_id,
180
r_hdr.so_no,
181
r_hdr.so_date,
182
r_hdr.type
183
);
184
185
FOR r_det IN
186
SELECT *
187
FROM public.invoice_details
188
WHERE invoice_header_id = r_hdr.id
189
LOOP
190
CALL public.create_invoice_detail(
191
v_new_invoice_id,
192
r_det.price,
193
r_det.quantity,
194
r_det.discount,
195
r_det.fees,
196
r_det.cgst_amount,
197
r_det.igst_amount,
198
r_det.sgst_amount,
199
r_det.taxable_amount,
200
r_det.total_amount,
201
r_det.serial_number,
202
r_det.product_id
203
);
204
END LOOP;
205
206
ELSE
207
CALL public.create_draft_invoice_header(
208
v_new_invoice_id,
209
r_hdr.company_id,
210
r_hdr.customer_id,
211
r_hdr.debit_account_id,
212
r_hdr.credit_account_id,
213
p_execution_date,
214
r_hdr.due_date,
215
r_hdr.payment_term,
216
r_hdr.taxable_amount,
217
r_hdr.cgst_amount,
218
r_hdr.sgst_amount,
219
r_hdr.igst_amount,
220
r_hdr.total_amount,
221
r_hdr.discount,
222
r_hdr.fees,
223
r_hdr.round_off,
224
r_hdr.currency_id,
225
r_hdr.note,
226
1,
227
v_triggered_by,
228
r_hdr.invoice_voucher,
229
r_hdr.source_warehouse_id,
230
r_hdr.destination_warehouse_id,
231
r_hdr.so_no,
232
r_hdr.so_date,
233
r_hdr.type
234
);
235
236
FOR r_det IN
237
SELECT *
238
FROM public.draft_invoice_details
239
WHERE invoice_header_id = r_hdr.id
240
LOOP
241
CALL public.create_draft_invoice_detail(
242
v_new_invoice_id,
243
r_det.price,
244
r_det.quantity,
245
r_det.discount,
246
r_det.fees,
247
r_det.cgst_amount,
248
r_det.igst_amount,
249
r_det.sgst_amount,
250
r_det.taxable_amount,
251
r_det.total_amount,
252
r_det.serial_number,
253
r_det.product_id
254
);
255
END LOOP;
256
END IF;
257
258
INSERT INTO public.schedule_execution_logs (
259
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
260
)
261
VALUES (
262
v_log_id, r_template.id, v_expected_execution_date, NOW(), 'Success', '', v_triggered_by, false
263
);
264
265
RAISE NOTICE 'Executed schedule % for date % successfully (new invoice: %).',
266
r_template.id, v_expected_execution_date, v_new_invoice_id;
267
268
PERFORM pg_advisory_unlock(hashtextextended(r_template.id::text, 0));
269
270
EXCEPTION WHEN OTHERS THEN
271
INSERT INTO public.schedule_execution_logs (
272
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
273
)
274
VALUES (
275
COALESCE(v_log_id, gen_random_uuid()), r_template.id,
276
v_expected_execution_date, NOW(), 'Failure', SQLERRM, v_triggered_by, false
277
);
278
279
PERFORM pg_advisory_unlock(hashtextextended(r_template.id::text, 0));
280
281
RAISE NOTICE 'Error executing schedule % for date %: %',
282
r_template.id, v_expected_execution_date, SQLERRM;
283
END;
284
END LOOP;
285
286
RAISE NOTICE 'Schedule execution completed for date: %', p_execution_date;
287
END;
288
$procedure$
|
|||||
| Procedure | save_company_prefix_for_invoice_and_payment | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.save_company_prefix_for_invoice_and_payment(IN p_company_id uuid, IN p_fin_year integer, IN p_invoice_prefix text, IN p_invoice_length integer, IN p_draft_invoice_prefix text, IN p_draft_invoice_length integer, IN p_group_invoice_prefix text, IN p_group_invoice_length integer, IN p_payment_prefix text, IN p_payment_length integer)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_new_id INTEGER;
6
BEGIN
7
-- Handle invoice_header_ids
8
IF EXISTS (SELECT 1 FROM public.invoice_header_ids WHERE company_id = p_company_id AND fin_year = p_fin_year) THEN
9
UPDATE public.invoice_header_ids
10
SET
11
invoice_prefix = COALESCE(p_invoice_prefix, invoice_prefix),
12
invoice_length = COALESCE(p_invoice_length, invoice_length)
13
WHERE company_id = p_company_id AND fin_year = p_fin_year;
14
ELSE
15
INSERT INTO public.invoice_header_ids (id, company_id, fin_year, invoice_prefix, invoice_length, last_invoice_id)
16
VALUES ((SELECT COALESCE(MAX(id), 0) + 1 FROM invoice_header_ids), p_company_id, p_fin_year, p_invoice_prefix, p_invoice_length, 0);
17
END IF;
18
19
-- Handle draft_invoice_header_ids
20
IF EXISTS (SELECT 1 FROM public.draft_invoice_header_ids WHERE company_id = p_company_id AND fin_year = p_fin_year) THEN
21
UPDATE public.draft_invoice_header_ids
22
SET
23
invoice_prefix = COALESCE(p_draft_invoice_prefix, invoice_prefix),
24
invoice_length = COALESCE(p_draft_invoice_length, invoice_length)
25
WHERE company_id = p_company_id AND fin_year = p_fin_year;
26
ELSE
27
INSERT INTO public.draft_invoice_header_ids (id, company_id, fin_year, invoice_prefix, invoice_length, last_invoice_id)
28
VALUES ((SELECT COALESCE(MAX(id), 0) + 1 FROM draft_invoice_header_ids), p_company_id, p_fin_year, p_draft_invoice_prefix, p_draft_invoice_length, 0);
29
END IF;
30
31
-- Handle group_invoice_header_ids
32
IF EXISTS (SELECT 1 FROM public.group_invoice_header_ids WHERE company_id = p_company_id AND fin_year = p_fin_year) THEN
33
UPDATE public.group_invoice_header_ids
34
SET
35
group_invoice_prefix = COALESCE(p_group_invoice_prefix, group_invoice_prefix),
36
group_invoice_length = COALESCE(p_group_invoice_length, group_invoice_length)
37
WHERE company_id = p_company_id AND fin_year = p_fin_year;
38
ELSE
39
INSERT INTO public.group_invoice_header_ids (id,company_id, fin_year, group_invoice_prefix, group_invoice_length, last_group_invoice_id)
40
VALUES ((SELECT COALESCE(MAX(id), 0) + 1 FROM group_invoice_header_ids),p_company_id, p_fin_year, p_group_invoice_prefix, p_group_invoice_length, 0);
41
END IF;
42
43
-- Handle invoice_payment_header_ids
44
IF EXISTS (SELECT 1 FROM public.invoice_payment_header_ids WHERE company_id = p_company_id AND fin_year = p_fin_year) THEN
45
UPDATE public.invoice_payment_header_ids
46
SET
47
payment_prefix = COALESCE(p_payment_prefix, payment_prefix),
48
payment_length = COALESCE(p_payment_length, payment_length)
49
WHERE company_id = p_company_id AND fin_year = p_fin_year;
50
ELSE
51
INSERT INTO public.invoice_payment_header_ids (id, company_id, fin_year, payment_prefix, payment_length, last_payment_id)
52
VALUES ((SELECT COALESCE(MAX(id), 0) + 1 FROM invoice_payment_header_ids), p_company_id, p_fin_year, p_payment_prefix, p_payment_length, 0);
53
END IF;
54
55
END;
56
$procedure$
|
|||||
| Procedure | transfer_draft_to_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.transfer_draft_to_invoice(IN p_draft_invoice_id uuid, IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_draft_invoice_header RECORD;
6
v_draft_invoice_detail RECORD;
7
BEGIN
8
-- Fetch the draft invoice header
9
SELECT * INTO v_draft_invoice_header
10
FROM public.draft_invoice_headers
11
WHERE id = p_draft_invoice_id;
12
13
IF v_draft_invoice_header IS NULL THEN
14
RAISE EXCEPTION 'Draft invoice not found: %', p_draft_invoice_id;
15
END IF;
16
17
-- Call to create invoice header using the procedure
18
CALL public.create_invoice_header(
19
v_draft_invoice_header.id,
20
v_draft_invoice_header.company_id,
21
v_draft_invoice_header.customer_id,
22
v_draft_invoice_header.debit_account_id,
23
v_draft_invoice_header.credit_account_id,
24
v_draft_invoice_header.invoice_date::date,
25
v_draft_invoice_header.due_date::date,
26
v_draft_invoice_header.payment_term,
27
v_draft_invoice_header.taxable_amount,
28
v_draft_invoice_header.cgst_amount,
29
v_draft_invoice_header.sgst_amount,
30
v_draft_invoice_header.igst_amount,
31
v_draft_invoice_header.total_amount,
32
v_draft_invoice_header.discount,
33
v_draft_invoice_header.fees,
34
v_draft_invoice_header.round_off,
35
v_draft_invoice_header.currency_id,
36
v_draft_invoice_header.note,
37
v_draft_invoice_header.invoice_status_id,
38
v_draft_invoice_header.created_by,
39
v_draft_invoice_header.invoice_voucher,
40
v_draft_invoice_header.source_warehouse_id,
41
v_draft_invoice_header.destination_warehouse_id,
42
v_draft_invoice_header.so_no,
43
v_draft_invoice_header.so_date::date,
44
v_draft_invoice_header.type
45
);
46
47
-- Raise a notice to indicate success
48
RAISE NOTICE 'Invoice header inserted with ID: %', v_draft_invoice_header.id;
49
50
-- Fetch and insert invoice details
51
FOR v_draft_invoice_detail IN
52
SELECT * FROM public.draft_invoice_details
53
WHERE invoice_header_id = p_draft_invoice_id
54
LOOP
55
INSERT INTO public.invoice_details(
56
id,
57
invoice_header_id,
58
product_id,
59
price,
60
quantity,
61
fees,
62
discount,
63
taxable_amount,
64
sgst_amount,
65
cgst_amount,
66
igst_amount,
67
total_amount,
68
serial_number)
69
VALUES(
70
v_draft_invoice_detail.id,
71
v_draft_invoice_detail.invoice_header_id,
72
v_draft_invoice_detail.product_id,
73
v_draft_invoice_detail.price,
74
v_draft_invoice_detail.quantity,
75
v_draft_invoice_detail.fees,
76
v_draft_invoice_detail.discount,
77
v_draft_invoice_detail.taxable_amount,
78
v_draft_invoice_detail.sgst_amount,
79
v_draft_invoice_detail.cgst_amount,
80
v_draft_invoice_detail.igst_amount,
81
v_draft_invoice_detail.total_amount,
82
v_draft_invoice_detail.serial_number
83
);
84
END LOOP;
85
86
-- Mark all draft invoice details as deleted after processing
87
UPDATE public.draft_invoice_details
88
SET
89
is_deleted = TRUE,
90
deleted_on_utc = now()
91
WHERE invoice_header_id = p_draft_invoice_id;
92
93
-- Mark the draft invoice as deleted
94
UPDATE public.draft_invoice_headers
95
SET
96
modified_by = p_modified_by,
97
is_deleted = TRUE,
98
deleted_on_utc = now()
99
WHERE id = p_draft_invoice_id;
100
END;
101
$procedure$
|
|||||
| Procedure | update_account_level_approval_configuration | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_account_level_approval_configuration(IN p_company_id uuid, IN p_account_id uuid, IN p_status_id integer, IN p_required_approval_levels integer, IN p_approvals jsonb, IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
approval_item JSONB;
6
existing_id INT;
7
BEGIN
8
-- 1️⃣ Soft delete old records not present in incoming approvals:
9
UPDATE invoice_approval_users_account
10
SET is_deleted = TRUE,
11
deleted_on_utc = NOW(),
12
modified_on_utc = NOW(),
13
modified_by = p_modified_by
14
WHERE company_id = p_company_id
15
AND account_id = p_account_id
16
AND NOT EXISTS (
17
SELECT 1
18
FROM jsonb_array_elements(p_approvals) AS elem
19
WHERE (elem->>'userId')::UUID = invoice_approval_users_account.user_id
20
AND (elem->>'approvalLevel')::INT = invoice_approval_users_account.approval_level
21
);
22
23
-- 2️⃣ Upsert incoming approvals:
24
FOR approval_item IN SELECT * FROM jsonb_array_elements(p_approvals)
25
LOOP
26
SELECT id INTO existing_id
27
FROM invoice_approval_users_account
28
WHERE company_id = p_company_id
29
AND account_id = p_account_id
30
AND user_id = (approval_item->>'userId')::UUID
31
AND approval_level = (approval_item->>'approvalLevel')::INT
32
AND is_deleted = FALSE
33
LIMIT 1;
34
35
IF existing_id IS NOT NULL THEN
36
UPDATE invoice_approval_users_account
37
SET status_id = (approval_item->>'statusId')::INT,
38
modified_on_utc = NOW(),
39
modified_by = p_modified_by
40
WHERE id = existing_id;
41
ELSE
42
INSERT INTO invoice_approval_users_account (
43
company_id,
44
account_id,
45
status_id,
46
user_id,
47
approval_level,
48
is_deleted,
49
created_on_utc,
50
created_by
51
)
52
VALUES (
53
p_company_id,
54
p_account_id,
55
(approval_item->>'statusId')::INT,
56
(approval_item->>'userId')::UUID,
57
(approval_item->>'approvalLevel')::INT,
58
FALSE,
59
NOW(),
60
p_modified_by
61
);
62
END IF;
63
END LOOP;
64
65
-- 3️⃣ Upsert invoice_account_approval_levels:
66
IF EXISTS (
67
SELECT 1
68
FROM invoice_account_approval_levels
69
WHERE company_id = p_company_id
70
AND account_id = p_account_id
71
AND status_id = 2 -- temparary updating only for status 2
72
) THEN
73
UPDATE invoice_account_approval_levels
74
SET approval_level_required = p_required_approval_levels,
75
status_id = 2,
76
modified_on_utc = NOW(),
77
modified_by = p_modified_by
78
WHERE company_id = p_company_id
79
AND account_id = p_account_id;
80
--AND status_id = p_status_id;
81
ELSE
82
INSERT INTO invoice_account_approval_levels (
83
company_id,
84
account_id,
85
status_id,
86
approval_level_required,
87
created_on_utc,
88
created_by
89
)
90
VALUES (
91
p_company_id,
92
p_account_id,
93
2,
94
p_required_approval_levels,
95
NOW(),
96
p_modified_by
97
);
98
END IF;
99
100
END;
101
$procedure$
|
|||||
| Procedure | update_apartment_invoice_template | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_apartment_invoice_template(IN p_calculation_type integer, IN p_company_id uuid, IN p_account_receivable_id uuid, IN p_sales_account_id uuid, IN p_invoice_date date, IN p_starts_from date, IN p_end_date date, IN p_payment_term integer, IN p_currency_id integer, IN p_fixed_product_id uuid, IN p_bhk_product_id uuid, IN p_sqft_product_id uuid, IN p_fixed_product_rate numeric, IN p_bhk_product_rate numeric, IN p_sqft_product_rate numeric, IN p_note character varying, IN p_invoice_status_id integer, IN p_due_days integer, IN p_billing_cycle text, IN p_created_by uuid, IN p_units json, IN p_invoice_template_id uuid, IN p_active_status boolean, IN p_day_of_week integer DEFAULT NULL::integer, IN p_day_of_month integer DEFAULT NULL::integer, IN p_month_of_year integer DEFAULT NULL::integer, IN p_quarters_of_month integer DEFAULT NULL::integer, IN p_half_year integer DEFAULT NULL::integer, IN p_bi_month integer DEFAULT NULL::integer, IN p_time_of_day interval DEFAULT '00:00:00'::interval)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_unit json;
6
v_unit_id integer;
7
v_bhk numeric;
8
v_sqft_area numeric;
9
v_customer_id uuid;
10
v_i integer := 0;
11
v_day_of_week integer := COALESCE(p_day_of_week, 0);
12
v_day_of_month integer := COALESCE(p_day_of_month, 0);
13
v_month_of_year integer := COALESCE(p_month_of_year, 0);
14
v_quarters_of_month integer := COALESCE(p_quarters_of_month, 0);
15
v_half_year integer := COALESCE(p_half_year, 0);
16
v_bi_month integer := COALESCE(p_bi_month, 0);
17
v_time_of_day interval := COALESCE(p_time_of_day, '00:00:00'::interval);
18
BEGIN
19
-- Update apartment_invoice_templates
20
UPDATE public.group_invoice_templates
21
SET calculation_type_id = p_calculation_type,
22
account_receivable_id = p_account_receivable_id,
23
sales_account_id = p_sales_account_id,
24
invoice_date = p_invoice_date,
25
starts_from = p_starts_from,
26
end_date = p_end_date,
27
grace_period = p_payment_term,
28
due_days = p_due_days,
29
fixed_rate = p_fixed_product_rate,
30
bhk_rate = p_bhk_product_rate,
31
sqft_rate = p_sqft_product_rate,
32
invoice_description = p_note,
33
billing_cycle = p_billing_cycle,
34
currency_id = p_currency_id,
35
bhk_product_id = p_bhk_product_id,
36
fixed_product_id = p_fixed_product_id,
37
sqft_product_id = p_sqft_product_id,
38
created_by = p_created_by,
39
created_on_utc = NOW(),
40
company_id = p_company_id,
41
active_status = p_active_status
42
WHERE id = p_invoice_template_id;
43
44
-- Delete existing units related to the invoice template
45
DELETE FROM public.group_invoice_template_customers
46
WHERE group_invoice_template_id = p_invoice_template_id;
47
48
-- Loop through units JSON array and insert into apartment_invoice_template_units
49
FOR v_i IN 0 .. json_array_length(p_units) - 1 LOOP
50
v_unit := p_units->v_i;
51
v_unit_id := (v_unit->>'id')::integer;
52
v_bhk := (v_unit->>'bhk')::numeric;
53
v_sqft_area := (v_unit->>'sqftArea')::numeric;
54
v_customer_id := (v_unit->>'customerId')::uuid;
55
56
-- Check for null values
57
IF v_customer_id IS NULL THEN
58
RAISE EXCEPTION 'Customer ID cannot be null';
59
END IF;
60
61
INSERT INTO public.group_invoice_template_customers(
62
id, group_invoice_template_id, unit_id, bhk, sqft_area, customer_id, created_on_utc, created_by)
63
VALUES (
64
gen_random_uuid(), p_invoice_template_id, v_unit_id, v_bhk, v_sqft_area, v_customer_id, NOW(), p_created_by
65
);
66
END LOOP;
67
68
-- Update or Insert into batch_schedules based on billing cycle
69
DELETE FROM public.batch_schedules
70
WHERE group_invoice_template_id = p_invoice_template_id;
71
72
IF p_billing_cycle = 'Daily' THEN
73
-- Daily requires only the time_of_day
74
INSERT INTO public.batch_schedules(
75
group_invoice_template_id, billing_cycle, time_of_day, created_on_utc, created_by)
76
VALUES (
77
p_invoice_template_id, p_billing_cycle, v_time_of_day, NOW(), p_created_by
78
);
79
ELSIF p_billing_cycle = 'Weekly' THEN
80
-- Weekly requires day_of_week and time_of_day
81
INSERT INTO public.batch_schedules(
82
group_invoice_template_id, billing_cycle, day_of_week, time_of_day, created_on_utc, created_by)
83
VALUES (
84
p_invoice_template_id, p_billing_cycle, v_day_of_week, v_time_of_day, NOW(), p_created_by
85
);
86
ELSIF p_billing_cycle = 'Monthly' THEN
87
-- Requires day_of_month and time_of_day
88
INSERT INTO public.batch_schedules(
89
group_invoice_template_id, billing_cycle, day_of_month, time_of_day, created_on_utc, created_by)
90
VALUES (
91
p_invoice_template_id, p_billing_cycle, v_day_of_month, v_time_of_day, NOW(), p_created_by
92
);
93
ELSIF p_billing_cycle = 'Bi-Monthly' THEN
94
-- Bi-Monthly requires month_of_year, day_of_month, and time_of_day
95
INSERT INTO public.batch_schedules(
96
group_invoice_template_id, billing_cycle, bi_month, day_of_month, time_of_day, created_on_utc, created_by)
97
VALUES (
98
p_invoice_template_id, p_billing_cycle, v_bi_month, v_day_of_month, v_time_of_day, NOW(), p_created_by
99
);
100
ELSIF p_billing_cycle = 'Quarterly' THEN
101
-- Quarterly requires quarter_of_year, month_of_year, day_of_month, and time_of_day
102
INSERT INTO public.batch_schedules(
103
group_invoice_template_id, billing_cycle, quarters_of_month, day_of_month, time_of_day, created_on_utc, created_by)
104
VALUES (
105
p_invoice_template_id, p_billing_cycle, v_quarters_of_month, v_day_of_month, v_time_of_day, NOW(), p_created_by
106
);
107
ELSIF p_billing_cycquarters_of_monthle = 'Half-yearly' THEN
108
-- Half-yearly requires half_year month_of_year, day_of_month, and time_of_day
109
INSERT INTO public.batch_schedules(
110
group_invoice_template_id, billing_cycle, half_year, day_of_month, time_of_day, created_on_utc, created_by)
111
VALUES (
112
p_invoice_template_id, p_billing_cycle, v_half_year, v_day_of_month, v_time_of_day, NOW(), p_created_by
113
);
114
ELSIF p_billing_cycle = 'Yearly' THEN
115
-- Yearly requires month_of_year, day_of_month, and time_of_day
116
INSERT INTO public.batch_schedules(
117
group_invoice_template_id, billing_cycle, month_of_year, day_of_month, time_of_day, created_on_utc, created_by)
118
VALUES (
119
p_invoice_template_id, p_billing_cycle, v_month_of_year, v_day_of_month, v_time_of_day, NOW(), p_created_by
120
);
121
ELSE
122
RAISE EXCEPTION 'Unknown billing cycle: %', p_billing_cycle;
123
END IF;
124
125
END;
126
$procedure$
|
|||||
| Procedure | update_company_level_approval_configuration | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_company_level_approval_configuration(IN p_company_id uuid, IN p_status_id integer, IN p_required_approval_levels integer, IN p_approvals jsonb, IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
5
DECLARE
6
approval_item JSONB;
7
existing_id INT;
8
BEGIN
9
-- 1️⃣ Soft delete old records not present in incoming approvals:
10
UPDATE invoice_approval_user_company
11
SET is_deleted = TRUE,
12
deleted_on_utc = NOW(),
13
modified_on_utc = NOW(),
14
modified_by = p_modified_by
15
WHERE company_id = p_company_id
16
AND NOT EXISTS (
17
SELECT 1
18
FROM jsonb_array_elements(p_approvals) AS elem
19
WHERE (elem->>'userId')::UUID = invoice_approval_user_company.user_id
20
AND (elem->>'statusId')::INT = invoice_approval_user_company.status_id
21
AND (elem->>'approvalLevel')::INT = invoice_approval_user_company.approval_level
22
);
23
24
-- 2️⃣ Upsert incoming approvals:
25
FOR approval_item IN SELECT * FROM jsonb_array_elements(p_approvals)
26
LOOP
27
SELECT id INTO existing_id
28
FROM invoice_approval_user_company
29
WHERE company_id = p_company_id
30
AND user_id = (approval_item->>'userId')::UUID
31
AND status_id = (approval_item->>'statusId')::INT
32
AND approval_level = (approval_item->>'approvalLevel')::INT
33
AND is_deleted = FALSE
34
LIMIT 1;
35
36
IF existing_id IS NOT NULL THEN
37
UPDATE invoice_approval_user_company
38
SET status_id = (approval_item->>'statusId')::INT,
39
modified_on_utc = NOW(),
40
modified_by = p_modified_by
41
WHERE id = existing_id;
42
ELSE
43
INSERT INTO invoice_approval_user_company (
44
company_id,
45
status_id,
46
user_id,
47
approval_level,
48
is_deleted,
49
created_on_utc,
50
created_by
51
)
52
VALUES (
53
p_company_id,
54
(approval_item->>'statusId')::INT,
55
(approval_item->>'userId')::UUID,
56
(approval_item->>'approvalLevel')::INT,
57
FALSE,
58
NOW(),
59
p_modified_by
60
);
61
END IF;
62
END LOOP;
63
64
-- 3️⃣ Update workflow approval level:
65
UPDATE invoice_workflow
66
SET approval_level = p_required_approval_levels
67
WHERE company_id = p_company_id
68
-- AND status = p_status_id; -- 🔔 use p_status_id here unless deliberately hardcoded as 2.
69
AND status = 2; -- 🔔 use p_status_id here unless deliberately hardcoded as 2.
70
71
END;
72
$procedure$
|
|||||
| Procedure | update_draft_invoice_next_status | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_draft_invoice_next_status(IN p_company_id uuid, IN p_invoice_ids uuid[], IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
5
DECLARE
6
v_invoice RECORD; -- Changed variable name from v_draft_invoice to v_invoice for clarity
7
v_account_id uuid;
8
v_current_approval_level integer;
9
v_approval_level_required integer;
10
v_next_status integer;
11
v_issue TEXT;
12
APPROVED_STATUS CONSTANT INTEGER := 3;
13
v_debug_next_status TEXT;
14
v_next_temp_id int;
15
BEGIN
16
RAISE NOTICE 'START update_draft_invoice_next_status for company %, user %', p_company_id, p_modified_by;
17
RAISE NOTICE 'Input invoice IDs: %', p_invoice_ids;
18
19
-- Loop through each draft invoice in the provided p_invoice_ids
20
FOR v_invoice IN
21
SELECT id, invoice_status_id, credit_account_id, current_approval_level
22
FROM public.draft_invoice_headers
23
WHERE id = ANY(p_invoice_ids) AND invoice_status_id = 2 -- Assuming status 2 is draft
24
LOOP
25
RAISE NOTICE 'Processing invoice ID: %', v_invoice.id;
26
27
v_account_id := v_invoice.credit_account_id;
28
v_current_approval_level := v_invoice.current_approval_level;
29
30
-- Get the total approval levels based on invoice status
31
SELECT approval_level_required
32
INTO v_approval_level_required
33
FROM public.invoice_approval_level_view
34
WHERE company_id = p_company_id
35
AND status_id = v_invoice.invoice_status_id
36
AND (
37
account_id = v_account_id -- Check for account-level entry first
38
OR account_id IS NULL -- Fallback to company-level entry
39
)
40
ORDER BY account_id ASC -- Ensure account-level entry is prioritized
41
LIMIT 1;
42
43
RAISE NOTICE 'Invoice % current approval level: %, required: %', v_invoice.id, v_current_approval_level, v_approval_level_required;
44
45
-- If no approval level found, log the issue and skip this invoice
46
IF v_approval_level_required IS NULL THEN
47
v_issue := 'Approval level not found';
48
-- Log the issue into a permanent table
49
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
50
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
51
52
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
53
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
54
VALUES (v_next_temp_id, v_invoice.id, 'Not found', v_issue);
55
56
RAISE NOTICE 'Invoice % skipped: approval level not found', v_invoice.id;
57
CONTINUE;
58
END IF;
59
60
-- Check if the user has permission to approve at the next level using the view
61
62
PERFORM public.check_invoice_approval_permissions(p_company_id,
63
v_invoice.invoice_status_id,
64
p_modified_by,
65
v_account_id,
66
(v_invoice.current_approval_level + 1));
67
68
-- If not authorized at account level, check company level
69
IF NOT FOUND THEN
70
v_issue := 'User not authorized to approve at the next level';
71
-- Log the issue into a permanent table
72
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
73
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
74
75
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
76
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
77
VALUES (v_next_temp_id, v_invoice.id, 'User not authorized', v_issue);
78
79
RAISE NOTICE 'Invoice % skipped: user not authorized', v_invoice.id;
80
CONTINUE;
81
END IF;
82
83
-- Compute next status for debug output
84
IF v_current_approval_level + 1 < v_approval_level_required THEN
85
v_debug_next_status := CONCAT('Level ', v_current_approval_level + 1);
86
ELSE
87
v_debug_next_status := 'Approved';
88
END IF;
89
90
RAISE NOTICE 'Invoice % next computed status: %', v_invoice.id, v_debug_next_status;
91
92
-- Check if the current approval level + 1 is less than the required level
93
IF v_approval_level_required > (v_current_approval_level + 1) THEN
94
-- Move to the next approval level
95
UPDATE public.draft_invoice_headers
96
SET current_approval_level = (v_current_approval_level + 1),
97
modified_by = p_modified_by,
98
modified_on_utc = now()
99
WHERE id = v_invoice.id;
100
101
-- Log the action for this level
102
INSERT INTO public.invoice_approval_logs(
103
invoice_id,
104
status_id,
105
approved_by,
106
approved_on,
107
"comment",
108
created_on_utc,
109
created_by,
110
approval_level
111
)
112
VALUES(
113
v_invoice.id,
114
v_invoice.invoice_status_id,
115
p_modified_by,
116
now(),
117
CONCAT('Approved at level ', v_current_approval_level + 1),
118
now(),
119
p_modified_by,
120
v_current_approval_level + 1
121
);
122
123
124
RAISE NOTICE 'Invoice % moved to next approval level: %', v_invoice.id, v_current_approval_level + 1;
125
126
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
127
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
128
VALUES (v_next_temp_id, v_invoice.id, v_debug_next_status, NULL);
129
130
ELSE
131
-- If the current approval level matches the required level, finalize the invoice
132
-- Finalize the invoice, move to the final status
133
UPDATE public.draft_invoice_headers
134
SET invoice_status_id = APPROVED_STATUS,
135
modified_by = p_modified_by,
136
modified_on_utc = now(),
137
current_approval_level = (v_current_approval_level + 1)
138
WHERE id = v_invoice.id;
139
140
-- Log the final approval action
141
INSERT INTO public.invoice_approval_logs(
142
invoice_id,
143
status_id,
144
approved_by,
145
approved_on,
146
"comment",
147
created_on_utc,
148
created_by,
149
approval_level
150
)
151
VALUES(
152
v_invoice.id,
153
v_invoice.invoice_status_id,
154
p_modified_by,
155
now(),
156
'Final Approval',
157
now(),
158
p_modified_by,
159
(v_current_approval_level + 1)
160
);
161
162
-- Move draft invoice to final invoice status
163
CALL transfer_draft_to_invoice(v_invoice.id, p_modified_by);
164
RAISE NOTICE 'Invoice % approved and transferred', v_invoice.id;
165
166
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
167
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
168
VALUES (v_next_temp_id, v_invoice.id, v_debug_next_status, NULL);
169
170
END IF;
171
END LOOP;
172
RAISE NOTICE 'END update_draft_invoice_next_status';
173
174
END
175
$procedure$
|
|||||
| Procedure | update_draft_invoice_next_status | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_draft_invoice_next_status(IN p_company_id uuid, IN p_invoice_status_id integer, IN p_invoice_ids uuid[], IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
5
DECLARE
6
v_invoice RECORD; -- Changed variable name from v_draft_invoice to v_invoice for clarity
7
v_account_id uuid;
8
v_current_approval_level integer;
9
v_approval_level_required integer;
10
v_next_status integer;
11
v_has_permission BOOLEAN;
12
v_issue TEXT;
13
APPROVED_STATUS CONSTANT INTEGER := 3;
14
v_debug_next_status varchar;
15
v_next_temp_id int;
16
BEGIN
17
-- Loop through each draft invoice in the provided p_invoice_ids
18
FOR v_invoice IN
19
SELECT id, invoice_status_id, credit_account_id, current_approval_level
20
FROM public.draft_invoice_headers
21
WHERE id = ANY(p_invoice_ids) AND invoice_status_id = 2 -- Assuming status 2 is draft
22
LOOP
23
v_account_id := v_invoice.credit_account_id;
24
v_current_approval_level := v_invoice.current_approval_level;
25
26
-- Get the total approval levels based on invoice status
27
SELECT approval_level_required
28
INTO v_approval_level_required
29
FROM public.invoice_approval_level_view
30
WHERE company_id = p_company_id
31
AND status_id = p_invoice_status_id
32
AND (
33
account_id = v_account_id -- Check for account-level entry first
34
OR account_id IS NULL -- Fallback to company-level entry
35
)
36
ORDER BY account_id ASC -- Ensure account-level entry is prioritized
37
LIMIT 1;
38
39
-- If no approval level found, log the issue and skip this invoice
40
IF v_approval_level_required IS NULL THEN
41
v_issue := 'Approval level not found';
42
-- Log the issue into a permanent table
43
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
44
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
45
46
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
47
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
48
VALUES (v_next_temp_id, v_invoice.id, 'Not found', v_issue);
49
CONTINUE;
50
END IF;
51
52
-- Check if the user has permission to approve at the next level using the view
53
SELECT EXISTS (
54
SELECT 1
55
FROM public.vw_invoice_approval_permissions
56
WHERE company_id = p_company_id
57
AND status_id = p_invoice_status_id
58
AND user_id = p_modified_by
59
AND account_approval_level = v_current_approval_level + 1
60
) INTO v_has_permission;
61
62
-- If not authorized at account level, check company level
63
IF NOT v_has_permission THEN
64
v_issue := 'User not authorized to approve at the next level';
65
-- Log the issue into a permanent table
66
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
67
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
68
69
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
70
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
71
VALUES (v_next_temp_id, v_invoice.id, 'User not authorized', v_issue);
72
CONTINUE; -- Skip to the next invoice
73
END IF;
74
75
-- Compute next status for debug output
76
IF v_current_approval_level + 1 < v_approval_level_required THEN
77
v_debug_next_status := CONCAT('Level ', v_current_approval_level + 1);
78
ELSE
79
v_debug_next_status := 'Approved';
80
END IF;
81
82
-- Check if the current approval level + 1 is less than the required level
83
IF v_current_approval_level + 1 < v_approval_level_required THEN
84
-- Move to the next approval level
85
UPDATE public.draft_invoice_headers
86
SET current_approval_level = v_current_approval_level + 1,
87
modified_by = p_modified_by,
88
modified_on_utc = now()
89
WHERE id = v_invoice.id;
90
91
-- Log the action for this level
92
INSERT INTO public.invoice_approval_logs(
93
invoice_id,
94
status_id,
95
approved_by,
96
approved_on,
97
"comment",
98
created_on_utc,
99
created_by,
100
approval_level
101
)
102
VALUES(
103
v_invoice.id,
104
p_invoice_status_id,
105
p_modified_by,
106
now(),
107
CONCAT('Approved at level ', v_current_approval_level + 1),
108
now(),
109
p_modified_by,
110
v_current_approval_level + 1
111
);
112
113
114
RAISE NOTICE 'Invoice % moved to next approval level: %', v_invoice.id, v_current_approval_level + 1;
115
116
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
117
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
118
VALUES (v_next_temp_id, v_invoice.id, v_debug_next_status, NULL);
119
120
ELSE
121
-- If the current approval level matches the required level, finalize the invoice
122
-- Finalize the invoice, move to the final status
123
UPDATE public.draft_invoice_headers
124
SET invoice_status_id = APPROVED_STATUS,
125
modified_by = p_modified_by,
126
modified_on_utc = now(),
127
current_approval_level = v_current_approval_level + 1
128
WHERE id = v_invoice.id;
129
130
-- Log the final approval action
131
INSERT INTO public.invoice_approval_logs(
132
invoice_id,
133
status_id,
134
approved_by,
135
approved_on,
136
"comment",
137
created_on_utc,
138
created_by,
139
approval_level
140
)
141
VALUES(
142
v_invoice.id,
143
p_invoice_status_id,
144
p_modified_by,
145
now(),
146
'Final Approval',
147
now(),
148
p_modified_by,
149
v_current_approval_level + 1
150
);
151
152
-- Move draft invoice to final invoice status
153
CALL transfer_draft_to_invoice(v_invoice.id, p_modified_by);
154
RAISE NOTICE 'Invoice % approved and transferred', v_invoice.id;
155
156
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
157
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
158
VALUES (v_next_temp_id, v_invoice.id, v_debug_next_status, NULL);
159
160
END IF;
161
END LOOP;
162
163
END
164
$procedure$
|
|||||
| Procedure | update_invoice_approval_user | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_invoice_approval_user(IN p_updates jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
update_item jsonb;
6
record_exists int;
7
BEGIN
8
FOR update_item IN SELECT * FROM jsonb_array_elements(p_updates)
9
LOOP
10
-- Check if record exists by ID (even if soft-deleted)
11
SELECT COUNT(*) INTO record_exists
12
FROM public.invoice_approval_user_company
13
WHERE id = (update_item->>'Id')::int;
14
15
IF record_exists > 0 THEN
16
-- Update existing record and reset is_deleted = false if needed
17
UPDATE public.invoice_approval_user_company
18
SET
19
company_id = (update_item->>'CompanyId')::uuid,
20
status_id = (update_item->>'StatusId')::int,
21
user_id = (update_item->>'UserId')::uuid,
22
approval_level = (update_item->>'ApprovalLevel')::int,
23
modified_by = (update_item->>'ModifiedBy')::uuid,
24
modified_on_utc = NOW(),
25
is_deleted = false
26
WHERE id = (update_item->>'Id')::int;
27
28
ELSE
29
-- Insert new record
30
INSERT INTO public.invoice_approval_user_company (
31
company_id,
32
status_id,
33
user_id,
34
approval_level,
35
created_on_utc,
36
modified_on_utc,
37
deleted_on_utc,
38
is_deleted,
39
created_by,
40
modified_by
41
) VALUES (
42
(update_item->>'CompanyId')::uuid,
43
(update_item->>'StatusId')::int,
44
(update_item->>'UserId')::uuid,
45
(update_item->>'ApprovalLevel')::int,
46
NOW(),
47
NULL,
48
NULL,
49
FALSE,
50
(update_item->>'ModifiedBy')::uuid,
51
NULL
52
);
53
END IF;
54
END LOOP;
55
END;
56
$procedure$
|
|||||
| Procedure | update_invoice_company_approval_user | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_invoice_company_approval_user(IN p_updates jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
update_item jsonb;
6
record_exists int;
7
BEGIN
8
FOR update_item IN SELECT * FROM jsonb_array_elements(p_updates)
9
LOOP
10
-- Check if record exists regardless of is_deleted
11
SELECT COUNT(*) INTO record_exists
12
FROM public.invoice_approval_user_company
13
WHERE id = (update_item->>'Id')::int;
14
15
IF record_exists > 0 THEN
16
-- Update record, remove is_deleted condition and optionally reset is_deleted to false
17
UPDATE public.invoice_approval_user_company
18
SET
19
company_id = (update_item->>'CompanyId')::uuid,
20
status_id = (update_item->>'StatusId')::int,
21
user_id = (update_item->>'UserId')::uuid,
22
approval_level = (update_item->>'ApprovalLevel')::int,
23
modified_by = (update_item->>'ModifiedBy')::uuid,
24
modified_on_utc = now(),
25
is_deleted = false -- optional: reactivate if was deleted
26
WHERE id = (update_item->>'Id')::int;
27
ELSE
28
-- Insert new record
29
INSERT INTO public.invoice_approval_user_company
30
(
31
company_id,
32
status_id,
33
user_id,
34
created_on_utc,
35
modified_on_utc,
36
deleted_on_utc,
37
is_deleted,
38
created_by,
39
modified_by,
40
approval_level
41
) VALUES (
42
(update_item->>'CompanyId')::uuid,
43
(update_item->>'StatusId')::int,
44
(update_item->>'UserId')::uuid,
45
now(),
46
null,
47
null,
48
false,
49
(update_item->>'ModifiedBy')::uuid,
50
null,
51
(update_item->>'ApprovalLevel')::int
52
);
53
END IF;
54
END LOOP;
55
END;
56
$procedure$
|
|||||
| Procedure | upsert_invoice_status_company_config_json | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.upsert_invoice_status_company_config_json(IN p_updates jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_item JSONB;
6
BEGIN
7
FOR v_item IN SELECT * FROM jsonb_array_elements(p_updates)
8
LOOP
9
PERFORM public.upsert_invoice_status_company_config(
10
(v_item->>'CompanyId')::UUID,
11
(v_item->>'StatusId')::INT,
12
(v_item->>'IsEnabled')::BOOLEAN,
13
(v_item->>'UserId')::UUID
14
);
15
END LOOP;
16
END;
17
$procedure$
|
|||||
| Procedure | update_invoice_next_status | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_invoice_next_status(IN p_company_id uuid, IN p_invoice_status_id integer, IN p_invoice_ids uuid[], IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_draft_invoice RECORD;
6
v_next_status integer;
7
v_account_id uuid;
8
v_has_account_workflow BOOLEAN;
9
v_has_user_account_level_approval BOOLEAN;
10
v_has_user_company_level_approval BOOLEAN;
11
v_is_data_available_for_company BOOLEAN;
12
APPROVED_STATUS CONSTANT INTEGER := 3;
13
FIRST_LEVEL_APPROVAL CONSTANT INTEGER := 8;
14
SECOND_LEVEL_APPROVAL CONSTANT INTEGER := 9;
15
BEGIN
16
FOR v_draft_invoice IN
17
SELECT id, invoice_status_id,credit_account_id
18
FROM public.draft_invoice_headers
19
WHERE id = ANY(p_invoice_ids)
20
LOOP
21
v_account_id := v_draft_invoice.credit_account_id;
22
23
SELECT EXISTS (
24
SELECT 1 FROM public.invoice_account_workflows
25
WHERE company_id = p_company_id
26
AND account_id = v_account_id
27
AND status = v_draft_invoice.invoice_status_id
28
AND is_deleted = false
29
) INTO v_has_account_workflow;
30
31
IF v_has_account_workflow THEN
32
33
SELECT next_status INTO v_next_status
34
FROM public.invoice_account_workflows
35
WHERE company_id = p_company_id
36
AND account_id = v_account_id
37
AND status = v_draft_invoice.invoice_status_id
38
AND is_deleted = false
39
LIMIT 1;
40
41
SELECT EXISTS (
42
SELECT 1 FROM public.invoice_account_approval_users
43
WHERE company_id = p_company_id
44
AND account_id = v_account_id
45
AND status = v_next_status
46
AND user_id = p_modified_by
47
) INTO v_has_user_account_level_approval;
48
49
IF NOT v_has_user_account_level_approval THEN
50
RAISE EXCEPTION 'User % is not authorized to approve at this level for account % ', p_modified_by, v_account_id ;
51
EXIT;
52
END IF;
53
54
ELSE
55
v_next_status := p_invoice_status_id;
56
57
SELECT EXISTS (
58
SELECT 1 FROM public.invoice_approvals
59
WHERE company_id = p_company_id
60
) INTO v_is_data_available_for_company;
61
62
IF v_is_data_available_for_company THEN
63
64
SELECT EXISTS (
65
SELECT 1 FROM public.invoice_approvals
66
WHERE company_id = p_company_id
67
AND status_id = v_next_status
68
AND user_id = p_modified_by
69
) INTO v_has_user_company_level_approval;
70
71
IF NOT v_has_user_company_level_approval THEN
72
RAISE EXCEPTION 'User % is not authorized to approve at this level', p_modified_by;
73
EXIT;
74
END IF;
75
76
END IF;
77
78
END IF;
79
80
81
UPDATE public.draft_invoice_headers
82
SET invoice_status_id = v_next_status,
83
modified_by = p_modified_by,
84
modified_on_utc = now()
85
WHERE id = v_draft_invoice.id;
86
87
IF v_next_status = APPROVED_STATUS OR v_next_status = FIRST_LEVEL_APPROVAL OR v_next_status = SECOND_LEVEL_APPROVAL THEN
88
INSERT INTO public.invoice_approval_logs(
89
invoice_id,
90
status_id,
91
approved_by,
92
approved_on,
93
"comment",
94
created_on_utc,
95
created_by
96
)
97
VALUES(
98
v_draft_invoice.id,
99
v_next_status,
100
p_modified_by,
101
now(),
102
CASE
103
WHEN v_next_status = FIRST_LEVEL_APPROVAL THEN 'Level 1 Approval'
104
WHEN v_next_status = SECOND_LEVEL_APPROVAL THEN 'Level 2 Approval'
105
WHEN v_next_status = APPROVED_STATUS THEN 'Final Approval'
106
ELSE 'Status updated'
107
END,
108
now(),
109
p_modified_by
110
);
111
END IF;
112
113
IF p_invoice_status_id = APPROVED_STATUS THEN
114
CALL transfer_draft_to_invoice(v_draft_invoice.id, p_modified_by);
115
116
RAISE NOTICE 'Draft Invoice Approved. Transferring to Final Invoice: %', v_draft_invoice.id;
117
END IF;
118
END LOOP;
119
END
120
$procedure$
|
|||||
| Procedure | update_invoice_next_status_for_invoice_header | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_invoice_next_status_for_invoice_header(IN p_company_id uuid, IN p_invoice_ids uuid[], IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_invoice RECORD; -- Variable name for invoice headers
6
v_account_id uuid;
7
v_next_status integer;
8
v_next_status_name character varying;
9
v_issue TEXT;
10
v_next_temp_id int;
11
v_has_permission BOOLEAN; -- Permission check variable
12
BEGIN
13
RAISE NOTICE 'START update_invoice_next_status_for_invoice_header: company_id=%, invoice_ids=%, modified_by=%', p_company_id, p_invoice_ids, p_modified_by;
14
15
-- Loop through each invoice in the provided p_invoice_ids
16
FOR v_invoice IN
17
SELECT id, invoice_status_id, credit_account_id
18
FROM public.invoice_headers
19
WHERE id = ANY(p_invoice_ids)
20
LOOP
21
RAISE NOTICE 'Processing invoice ID: %', v_invoice.id;
22
23
v_account_id := v_invoice.credit_account_id;
24
25
-- Get the next status from invoice workflow
26
RAISE NOTICE 'Fetching next status for invoice ID: %, company_id: %, current status: %', v_invoice.id, p_company_id, v_invoice.invoice_status_id;
27
28
SELECT next_status INTO v_next_status
29
FROM public.invoice_workflow
30
WHERE company_id = p_company_id
31
AND status = v_invoice.invoice_status_id
32
AND is_deleted = false
33
AND is_enabled = true
34
LIMIT 1;
35
36
-- If no next status found, log the issue and skip this invoice
37
IF v_next_status IS NULL THEN
38
v_issue := 'Next status not found for company % and status %';
39
RAISE NOTICE 'Next status not found for company % and status %', p_company_id, v_invoice.invoice_status_id;
40
-- Log the issue into a permanent table
41
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
42
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
43
44
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
45
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
46
VALUES (v_next_temp_id, v_invoice.id, 'Not found', v_issue);
47
48
CONTINUE;
49
END IF;
50
51
RAISE NOTICE 'Next status for invoice ID %: %', v_invoice.id, v_next_status;
52
53
-- Check if the user has permission to move to the next status using the updated function
54
RAISE NOTICE 'Checking if user has permission to move invoice ID % to next status %', v_invoice.id, v_next_status;
55
56
-- Calling the updated check_invoice_approval_permissions function to check permission
57
PERFORM public.check_invoice_approval_permissions(
58
p_company_id,
59
v_next_status,
60
p_modified_by,
61
v_account_id,0);
62
63
-- Check if the permission was granted (if any row is returned)
64
IF NOT FOUND THEN
65
v_issue := 'User % is not authorized to move invoice to the next status for status %';
66
RAISE NOTICE 'User % is not authorized to move invoice ID % to next status %', p_modified_by, v_invoice.id, v_next_status;
67
-- Log the issue into a permanent table
68
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
69
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
70
71
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
72
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
73
VALUES (v_next_temp_id, v_invoice.id, 'User not authorized', v_issue);
74
75
CONTINUE; -- Skip this invoice and move to the next one
76
END IF;
77
78
-- Move to the next status
79
RAISE NOTICE 'Moving invoice ID % to the next status: %', v_invoice.id, v_next_status;
80
81
UPDATE public.invoice_headers
82
SET invoice_status_id = v_next_status, -- Update the status to the next status
83
modified_by = p_modified_by,
84
modified_on_utc = now()
85
WHERE id = v_invoice.id;
86
87
-- Log the action for moving to the next status
88
INSERT INTO public.invoice_approval_logs(
89
invoice_id,
90
status_id,
91
approved_by,
92
approved_on,
93
"comment",
94
created_on_utc,
95
created_by,
96
approval_level
97
)
98
VALUES(
99
v_invoice.id,
100
v_invoice.invoice_status_id,
101
p_modified_by,
102
now(),
103
'Approved and moved to next status', -- Updated comment
104
now(),
105
p_modified_by,
106
0 -- Setting approval_level to 0, as no level change is needed
107
);
108
109
-- Get the name of the next status for debug output
110
SELECT name INTO v_next_status_name
111
FROM public.invoice_statuses
112
WHERE id = v_next_status;
113
114
-- Insert into temp_invoice_next_status for debugging purposes
115
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
116
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
117
VALUES (v_next_temp_id, v_invoice.id, v_next_status_name, NULL);
118
END LOOP;
119
120
RAISE NOTICE 'END update_invoice_next_status_for_invoice_header';
121
122
END
123
$procedure$
|
|||||
| Procedure | update_invoice_next_status_for_invoice_header | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_invoice_next_status_for_invoice_header(IN p_company_id uuid, IN p_invoice_status_id integer, IN p_invoice_ids uuid[], IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
5
DECLARE
6
v_invoice RECORD; -- Variable name for invoice headers
7
v_account_id uuid;
8
v_next_status integer;
9
v_next_status_name character varying;
10
v_has_permission BOOLEAN;
11
v_issue TEXT;
12
v_next_temp_id int;
13
BEGIN
14
-- Loop through each invoice in the provided p_invoice_ids
15
FOR v_invoice IN
16
SELECT id, invoice_status_id, credit_account_id
17
FROM public.invoice_headers
18
WHERE id = ANY(p_invoice_ids)
19
LOOP
20
v_account_id := v_invoice.credit_account_id;
21
22
-- Get the next status from invoice workflow
23
SELECT next_status INTO v_next_status
24
FROM public.invoice_workflow
25
WHERE company_id = p_company_id
26
AND status = p_invoice_status_id
27
AND is_deleted = false
28
AND is_enabled = true
29
LIMIT 1;
30
31
-- If no next status found, log the issue and skip this invoice
32
IF v_next_status IS NULL THEN
33
v_issue := 'Next status not found for company % and status %';
34
-- Log the issue into a permanent table
35
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
36
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
37
38
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
39
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
40
VALUES (v_next_temp_id, v_invoice.id, 'Not found', v_issue);
41
42
CONTINUE;
43
END IF;
44
45
-- Check if the user has permission to move to the next status using the view
46
SELECT EXISTS (
47
SELECT 1
48
FROM public.vw_invoice_approval_permissions
49
WHERE company_id = p_company_id
50
AND status_id = v_next_status -- Check for the next status
51
AND user_id = p_modified_by
52
) INTO v_has_permission;
53
54
-- If not authorized to move to the next status, log the issue and skip the invoice
55
IF NOT v_has_permission THEN
56
v_issue := 'User % is not authorized to move invoice to the next status for status %';
57
-- Log the issue into a permanent table
58
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
59
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
60
61
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
62
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
63
VALUES (v_next_temp_id, v_invoice.id, 'User not authorized', v_issue);
64
65
CONTINUE; -- Skip this invoice and move to the next one
66
END IF;
67
68
-- Move to the next status
69
UPDATE public.invoice_headers
70
SET invoice_status_id = v_next_status, -- Update the status to the next status
71
modified_by = p_modified_by,
72
modified_on_utc = now()
73
WHERE id = v_invoice.id;
74
75
-- Log the action for moving to the next status
76
INSERT INTO public.invoice_approval_logs(
77
invoice_id,
78
status_id,
79
approved_by,
80
approved_on,
81
"comment",
82
created_on_utc,
83
created_by,
84
approval_level
85
)
86
VALUES(
87
v_invoice.id,
88
p_invoice_status_id,
89
p_modified_by,
90
now(),
91
'Approved and moved to next status', -- Updated comment
92
now(),
93
p_modified_by,
94
0 -- Setting approval_level to 0, as no level change is needed
95
);
96
SELECT name INTO v_next_status_name
97
FROM public.invoice_statuses
98
WHERE id = v_next_status;
99
100
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
101
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
102
VALUES (v_next_temp_id, v_invoice.id, v_next_status_name, NULL);
103
END LOOP;
104
END
105
$procedure$
|
|||||
| Procedure | update_invoice_previous_status | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_invoice_previous_status(IN p_company_id uuid, IN p_invoice_status_id integer, IN p_invoice_ids uuid[], IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_invoice RECORD;
6
v_minimum_status INTEGER := 0; -- Local variable for minimum status
7
BEGIN
8
-- Process invoices for previous status update
9
FOR v_invoice IN
10
SELECT id, invoice_status_id
11
FROM public.draft_invoice_headers
12
WHERE id = ANY(p_invoice_ids)
13
LOOP
14
-- Ensure the status doesn't go below the minimum allowed status
15
IF v_invoice.invoice_status_id > v_minimum_status THEN
16
-- Update the invoice to the previous status
17
UPDATE public.draft_invoice_headers
18
SET invoice_status_id = p_invoice_status_id,
19
modified_by = p_modified_by,
20
modified_on_utc = now()
21
WHERE id = v_invoice.id;
22
23
RAISE NOTICE 'Invoice status updated to previous status for Invoice ID: %', v_invoice.id;
24
ELSE
25
RAISE NOTICE 'Cannot decrement status below the minimum allowed status for Invoice ID: %', v_invoice.id;
26
END IF;
27
END LOOP;
28
29
END
30
$procedure$
|
|||||
| Procedure | clean_up_org_sales | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.clean_up_org_sales(IN p_organization_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_company_id uuid;
6
v_customer_id uuid;
7
v_customer_note_header_id uuid;
8
v_invoice_header_id uuid;
9
v_invoice_payment_header_id uuid;
10
v_draft_invoice_header_id uuid;
11
v_group_invoice_header_id uuid;
12
v_gate_pass_header_id uuid;
13
v_user_id uuid;
14
BEGIN
15
FOR v_company_id IN
16
SELECT id FROM companies WHERE organization_id = p_organization_id
17
LOOP
18
-- Customers and all their detail tables
19
FOR v_customer_id IN SELECT id FROM customers WHERE company_id = v_company_id LOOP
20
DELETE FROM customer_contacts WHERE customer_id = v_customer_id;
21
DELETE FROM customer_bank_accounts WHERE customer_id = v_customer_id;
22
DELETE FROM customer_upis WHERE customer_id = v_customer_id;
23
DELETE FROM customer_default_accounts WHERE customer_id = v_customer_id;
24
FOR v_customer_note_header_id IN SELECT id FROM customer_note_headers WHERE customer_id = v_customer_id LOOP
25
DELETE FROM customer_note_details WHERE customer_note_header_id = v_customer_note_header_id;
26
END LOOP;
27
DELETE FROM customer_note_headers WHERE customer_id = v_customer_id;
28
END LOOP;
29
DELETE FROM customers WHERE company_id = v_company_id;
30
DELETE FROM customer_note_statuses WHERE created_by = v_company_id OR modified_by = v_company_id;
31
DELETE FROM customer_note_work_flows WHERE company_id = v_company_id;
32
33
-- Draft Invoices and their details
34
FOR v_draft_invoice_header_id IN SELECT id FROM draft_invoice_headers WHERE company_id = v_company_id LOOP
35
DELETE FROM draft_invoice_details WHERE invoice_header_id = v_draft_invoice_header_id;
36
END LOOP;
37
DELETE FROM draft_invoice_headers WHERE company_id = v_company_id;
38
DELETE FROM draft_invoice_header_ids WHERE company_id = v_company_id;
39
40
-- Group Invoices and their details/templates
41
FOR v_group_invoice_header_id IN SELECT id FROM group_invoice_headers WHERE company_id = v_company_id LOOP
42
DELETE FROM group_invoice_details WHERE group_invoice_header_id = v_group_invoice_header_id;
43
END LOOP;
44
DELETE FROM group_invoice_headers WHERE company_id = v_company_id;
45
DELETE FROM group_invoice_header_ids WHERE company_id = v_company_id;
46
DELETE FROM group_invoice_templates WHERE company_id = v_company_id;
47
DELETE FROM group_invoice_template_customers WHERE customer_id IN (
48
SELECT id FROM customers WHERE company_id = v_company_id
49
);
50
51
-- Invoice Headers and all their details
52
FOR v_invoice_header_id IN SELECT id FROM invoice_headers WHERE company_id = v_company_id LOOP
53
DELETE FROM invoice_details WHERE invoice_header_id = v_invoice_header_id;
54
DELETE FROM invoice_approval_logs WHERE invoice_id = v_invoice_header_id;
55
DELETE FROM invoice_approval_issue_log WHERE invoice_id = v_invoice_header_id;
56
DELETE FROM invoice_penalties WHERE invoice_header_id = v_invoice_header_id;
57
DELETE FROM penalty_processing_logs WHERE invoice_id = v_invoice_header_id;
58
DELETE FROM recurring_sales_schedules WHERE invoice_header_id = v_invoice_header_id;
59
DELETE FROM group_invoice_details WHERE invoice_header_id = v_invoice_header_id;
60
END LOOP;
61
DELETE FROM invoice_headers WHERE company_id = v_company_id;
62
DELETE FROM invoice_header_ids WHERE company_id = v_company_id;
63
64
-- Invoice payments and their details
65
FOR v_invoice_payment_header_id IN SELECT id FROM invoice_payment_headers WHERE company_id = v_company_id LOOP
66
DELETE FROM invoice_payment_details WHERE invoice_payment_header_id = v_invoice_payment_header_id;
67
END LOOP;
68
DELETE FROM invoice_payment_headers WHERE company_id = v_company_id;
69
DELETE FROM invoice_payment_header_ids WHERE company_id = v_company_id;
70
71
-- Invoice workflow/configs
72
DELETE FROM invoice_workflow WHERE company_id = v_company_id;
73
DELETE FROM invoice_status_company_configs WHERE company_id = v_company_id;
74
DELETE FROM invoice_account_approval_levels WHERE company_id = v_company_id;
75
DELETE FROM invoice_approval_users_account WHERE company_id = v_company_id;
76
DELETE FROM invoice_approval_user_company WHERE company_id = v_company_id;
77
78
-- Invoice voucher ids
79
DELETE FROM invoice_voucher_ids WHERE company_id = v_company_id;
80
81
-- Penalties and configs
82
DELETE FROM penalty_configs WHERE company_id = v_company_id;
83
DELETE FROM penalty_frequencies WHERE created_by = v_company_id OR modified_by = v_company_id;
84
85
-- Warehouses (corrected)
86
DELETE FROM warehouses WHERE customer_id IN (
87
SELECT id FROM customers WHERE company_id = v_company_id
88
);
89
90
-- Gate pass and their details
91
FOR v_gate_pass_header_id IN SELECT id FROM gate_pass_headers WHERE company_id = v_company_id LOOP
92
DELETE FROM gate_pass_details WHERE gate_pass_header_id = v_gate_pass_header_id;
93
END LOOP;
94
DELETE FROM gate_pass_headers WHERE company_id = v_company_id;
95
96
-- Users and user roles
97
FOR v_user_id IN SELECT id FROM users WHERE company_id = v_company_id LOOP
98
DELETE FROM user_roles WHERE user_id = v_user_id;
99
END LOOP;
100
DELETE FROM users WHERE company_id = v_company_id;
101
102
-- Finally, company itself
103
DELETE FROM companies WHERE id = v_company_id;
104
END LOOP;
105
106
-- Organization record itself
107
DELETE FROM organizations WHERE id = p_organization_id;
108
109
RAISE NOTICE 'Organization % and all related sales data deleted.', p_organization_id;
110
END;
111
$procedure$
|
|||||
| Procedure | copy_draft_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.copy_draft_invoices(IN draft_invoice_ids uuid[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
original_draft_invoice RECORD;
6
original_draft_detail RECORD;
7
new_draft_header_id UUID;
8
BEGIN
9
-- Loop through each provided draft invoice ID
10
FOR original_draft_invoice IN
11
SELECT * FROM draft_invoice_headers WHERE id = ANY(draft_invoice_ids) AND is_deleted = FALSE
12
LOOP
13
RAISE NOTICE 'Processing draft invoice ID: %', original_draft_invoice.id;
14
15
-- Generate a new UUID for the draft invoice header
16
new_draft_header_id := gen_random_uuid();
17
RAISE NOTICE 'Generated new draft invoice header ID: %', new_draft_header_id;
18
19
-- Call the create_draft_invoice_header procedure to create the new draft invoice header
20
CALL create_draft_invoice_header(
21
new_draft_header_id,
22
original_draft_invoice.company_id,
23
original_draft_invoice.customer_id,
24
original_draft_invoice.debit_account_id,
25
original_draft_invoice.credit_account_id,
26
original_draft_invoice.invoice_date::date,
27
original_draft_invoice.due_date::date,
28
original_draft_invoice.payment_term,
29
original_draft_invoice.taxable_amount,
30
original_draft_invoice.cgst_amount,
31
original_draft_invoice.sgst_amount,
32
original_draft_invoice.igst_amount,
33
original_draft_invoice.total_amount,
34
original_draft_invoice.discount,
35
original_draft_invoice.fees,
36
original_draft_invoice.round_off,
37
original_draft_invoice.currency_id,
38
original_draft_invoice.note,
39
1, -- Set draft status ID
40
original_draft_invoice.created_by,
41
original_draft_invoice.invoice_voucher,
42
original_draft_invoice.source_warehouse_id,
43
original_draft_invoice.destination_warehouse_id,
44
original_draft_invoice.so_no,
45
original_draft_invoice.so_date::date,
46
original_draft_invoice.type
47
);
48
49
RAISE NOTICE 'Inserted new draft invoice header for original draft invoice ID: %', original_draft_invoice.id;
50
51
-- Loop through each detail for the current draft invoice and copy to new draft
52
FOR original_draft_detail IN
53
SELECT * FROM draft_invoice_details WHERE invoice_header_id = original_draft_invoice.id
54
LOOP
55
RAISE NOTICE 'Copying draft detail with serial number % for draft invoice ID: %', original_draft_detail.serial_number, original_draft_invoice.id;
56
57
-- Call create_invoice_detail to insert each detail
58
CALL create_draft_invoice_detail(
59
new_draft_header_id, -- Use the new draft invoice header ID
60
original_draft_detail.price,
61
original_draft_detail.quantity,
62
original_draft_detail.discount,
63
original_draft_detail.fees,
64
original_draft_detail.cgst_amount,
65
original_draft_detail.igst_amount,
66
original_draft_detail.sgst_amount,
67
original_draft_detail.taxable_amount,
68
original_draft_detail.total_amount,
69
original_draft_detail.serial_number,
70
original_draft_detail.product_id
71
);
72
73
RAISE NOTICE 'Inserted new draft invoice detail for draft header ID: %', new_draft_header_id;
74
END LOOP;
75
END LOOP;
76
END;
77
$procedure$
|
|||||
| Procedure | copy_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.copy_invoices(IN p_invoice_ids uuid[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_invoice_header RECORD;
6
v_invoice_detail RECORD;
7
v_new_header_id UUID;
8
9
-- Constants for statuses and other values
10
invoice_status_draft CONSTANT INTEGER := 1;
11
approved_invoice_status CONSTANT INTEGER := 3;
12
BEGIN
13
-- Loop through each provided invoice ID
14
FOR v_invoice_header IN
15
(
16
SELECT id, company_id, customer_id, debit_account_id, credit_account_id, invoice_date, due_date,
17
payment_term, taxable_amount, cgst_amount, sgst_amount, igst_amount, total_amount, discount,
18
fees, round_off, currency_id, note, created_by, invoice_voucher, source_warehouse_id,
19
destination_warehouse_id, so_no, so_date, type,invoice_status_id
20
FROM draft_invoice_headers
21
WHERE id = ANY(p_invoice_ids) AND is_deleted = false
22
UNION ALL
23
SELECT id, company_id, customer_id, debit_account_id, credit_account_id, invoice_date, due_date,
24
payment_term, taxable_amount, cgst_amount, sgst_amount, igst_amount, total_amount, discount,
25
fees, round_off, currency_id, note, created_by, invoice_voucher, source_warehouse_id,
26
destination_warehouse_id, so_no, so_date, type,invoice_status_id
27
FROM invoice_headers
28
WHERE id = ANY(p_invoice_ids)
29
)
30
LOOP
31
RAISE NOTICE 'Invoices id: %', v_invoice_header.id;
32
33
-- Generate a new UUID for the new invoice header
34
v_new_header_id := gen_random_uuid();
35
RAISE NOTICE 'Generated new invoice header ID: %', v_new_header_id;
36
37
-- Call the create_draft_invoice_header procedure to create the new invoice header
38
CALL create_draft_invoice_header(
39
v_new_header_id,
40
v_invoice_header.company_id,
41
v_invoice_header.customer_id,
42
v_invoice_header.debit_account_id,
43
v_invoice_header.credit_account_id,
44
v_invoice_header.invoice_date::date,
45
v_invoice_header.due_date::date,
46
v_invoice_header.payment_term,
47
v_invoice_header.taxable_amount,
48
v_invoice_header.cgst_amount,
49
v_invoice_header.sgst_amount,
50
v_invoice_header.igst_amount,
51
v_invoice_header.total_amount,
52
v_invoice_header.discount,
53
v_invoice_header.fees,
54
v_invoice_header.round_off,
55
v_invoice_header.currency_id,
56
v_invoice_header.note,
57
invoice_status_draft,
58
v_invoice_header.created_by,
59
v_invoice_header.invoice_voucher,
60
v_invoice_header.source_warehouse_id,
61
v_invoice_header.destination_warehouse_id,
62
v_invoice_header.so_no,
63
v_invoice_header.so_date::date,
64
v_invoice_header.type
65
);
66
67
RAISE NOTICE 'Inserted new invoice header for original invoice ID: %', v_invoice_header.id;
68
-- Loop through details based on whether the invoice is draft or not
69
70
IF v_invoice_header.invoice_status_id < approved_invoice_status THEN
71
FOR v_invoice_detail IN
72
SELECT * FROM draft_invoice_details WHERE invoice_header_id = v_invoice_header.id
73
LOOP
74
RAISE NOTICE 'Copying draft detail with serial number % for draft invoice ID: %', v_invoice_detail.serial_number, v_invoice_header.id;
75
76
-- Call create_draft_invoice_detail to insert each detail
77
CALL create_draft_invoice_detail(
78
v_new_header_id,
79
v_invoice_detail.price,
80
v_invoice_detail.quantity,
81
v_invoice_detail.discount,
82
v_invoice_detail.fees,
83
v_invoice_detail.cgst_amount,
84
v_invoice_detail.igst_amount,
85
v_invoice_detail.sgst_amount,
86
v_invoice_detail.taxable_amount,
87
v_invoice_detail.total_amount,
88
v_invoice_detail.serial_number,
89
v_invoice_detail.product_id
90
);
91
92
END LOOP;
93
ELSE
94
FOR v_invoice_detail IN
95
SELECT * FROM invoice_details WHERE invoice_header_id = v_invoice_header.id
96
LOOP
97
RAISE NOTICE 'Copying detail with serial number % for invoice ID: %', v_invoice_detail.serial_number, v_invoice_header.id;
98
99
-- Call create_draft_invoice_detail to insert each detail
100
CALL create_draft_invoice_detail(
101
v_new_header_id,
102
v_invoice_detail.price,
103
v_invoice_detail.quantity,
104
v_invoice_detail.discount,
105
v_invoice_detail.fees,
106
v_invoice_detail.cgst_amount,
107
v_invoice_detail.igst_amount,
108
v_invoice_detail.sgst_amount,
109
v_invoice_detail.taxable_amount,
110
v_invoice_detail.total_amount,
111
v_invoice_detail.serial_number,
112
v_invoice_detail.product_id
113
);
114
115
RAISE NOTICE 'Inserted detail for new invoice header ID: %', v_new_header_id;
116
END LOOP;
117
END IF;
118
END LOOP;
119
120
RAISE NOTICE 'Procedure completed successfully.';
121
END;
122
$procedure$
|
|||||
| Procedure | create_invoice_detail | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_invoice_detail(IN p_invoice_header_id uuid, IN p_price numeric, IN p_quantity numeric, IN p_discount numeric, IN p_fees numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_sgst_amount numeric, IN p_taxable_amount numeric, IN p_total_amount numeric, IN p_serial_number integer, IN p_product_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
p_id uuid;
6
BEGIN
7
p_id := gen_random_uuid();
8
9
INSERT INTO public.invoice_details(
10
id,
11
invoice_header_id,
12
price,
13
quantity,
14
cgst_amount,
15
discount,
16
fees,
17
igst_amount,
18
sgst_amount,
19
taxable_amount,
20
total_amount,
21
serial_number,
22
product_id
23
)
24
VALUES (
25
p_id,
26
p_invoice_header_id,
27
p_price,
28
p_quantity,
29
p_cgst_amount,
30
p_discount,
31
p_fees,
32
p_igst_amount,
33
p_sgst_amount,
34
p_taxable_amount,
35
p_total_amount,
36
p_serial_number,
37
p_product_id
38
);
39
END;
40
$procedure$
|
|||||
| Procedure | create_invoice_header | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_invoice_header(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_invoice_date date, IN p_due_date date, IN p_payment_term integer, IN p_taxable_amount numeric, IN p_cgst_amount numeric, IN p_sgst_amount numeric, IN p_igst_amount numeric, IN p_total_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_round_off numeric, IN p_currency_id integer, IN p_note character varying, IN p_invoice_status_id integer, IN p_created_by uuid, IN p_invoice_voucher character varying, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_so_no character varying, IN p_so_date date, IN p_type integer)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
invoice_number character varying;
6
BEGIN
7
invoice_number := get_new_invoice_number(p_company_id, p_invoice_date);
8
9
RAISE NOTICE 'Value: %', invoice_number;
10
11
INSERT INTO
12
public.invoice_headers(
13
id,
14
company_id,
15
customer_id,
16
credit_account_id,
17
debit_account_id,
18
invoice_number,
19
invoice_date,
20
due_date,
21
payment_term,
22
taxable_amount,
23
cgst_amount,
24
sgst_amount,
25
igst_amount,
26
total_amount,
27
discount,
28
fees,
29
round_off,
30
currency_id,
31
note,
32
invoice_status_id,
33
created_by,
34
invoice_voucher,
35
source_warehouse_id,
36
destination_warehouse_id,
37
created_on_utc,
38
so_no,
39
so_date,
40
type
41
)
42
VALUES (
43
p_id,
44
p_company_id,
45
p_customer_id,
46
p_credit_account_id,
47
p_debit_account_id,
48
invoice_number,
49
p_invoice_date,
50
p_due_date,
51
p_payment_term,
52
p_taxable_amount,
53
p_cgst_amount,
54
p_sgst_amount,
55
p_igst_amount,
56
p_total_amount,
57
p_discount,
58
p_fees,
59
p_round_off,
60
p_currency_id,
61
p_note,
62
p_invoice_status_id,
63
p_created_by,
64
p_invoice_voucher,
65
p_source_warehouse_id,
66
p_destination_warehouse_id,
67
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
68
p_so_no,
69
p_so_date,
70
p_type
71
);
72
END;
73
$procedure$
|
|||||
| Procedure | edit_draft_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.edit_draft_invoice(IN p_invoice_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_payment_term integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_round_off numeric, IN p_note text, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_modified_by uuid, IN p_invoice_details jsonb, IN p_type integer)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
detail RECORD; -- Record variable for each detail from the JSONB array
6
BEGIN
7
-- Check if the invoice exists and can be edited (status < 3)
8
IF EXISTS (
9
SELECT 1
10
FROM public.draft_invoice_headers
11
WHERE id = p_invoice_id
12
AND invoice_status_id < 3
13
) THEN
14
-- Update the draft invoice header
15
UPDATE public.draft_invoice_headers
16
SET
17
company_id = p_company_id,
18
customer_id = p_customer_id,
19
currency_id = p_currency_id,
20
invoice_date = p_invoice_date,
21
payment_term = p_payment_term,
22
due_date = p_due_date,
23
total_amount = p_total_amount,
24
taxable_amount = p_taxable_amount,
25
discount = p_discount,
26
fees = p_fees,
27
sgst_amount = p_sgst_amount,
28
cgst_amount = p_cgst_amount,
29
igst_amount = p_igst_amount,
30
round_off = p_round_off,
31
note = p_note,
32
source_warehouse_id = p_source_warehouse_id, -- New Parameter
33
destination_warehouse_id = p_destination_warehouse_id, -- New Parameter
34
modified_on_utc = now(),
35
modified_by = p_modified_by,
36
type = p_type
37
WHERE id = p_invoice_id;
38
39
-- Soft delete existing invoice details
40
UPDATE public.draft_invoice_details
41
SET is_deleted = true, deleted_on_utc = now()
42
WHERE invoice_header_id = p_invoice_id;
43
44
-- Insert or update the new invoice details from the JSONB
45
FOR detail IN
46
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
47
id uuid,
48
product_id uuid,
49
price numeric,
50
quantity numeric,
51
fees numeric,
52
discount numeric,
53
taxable_amount numeric,
54
sgst_amount numeric,
55
cgst_amount numeric,
56
igst_amount numeric,
57
total_amount numeric,
58
serial_number integer
59
)
60
LOOP
61
INSERT INTO public.draft_invoice_details (
62
id,
63
invoice_header_id,
64
product_id,
65
price,
66
quantity,
67
fees,
68
discount,
69
taxable_amount,
70
sgst_amount,
71
cgst_amount,
72
igst_amount,
73
total_amount,
74
serial_number,
75
is_deleted
76
) VALUES (
77
detail.id,
78
p_invoice_id,
79
detail.product_id,
80
detail.price,
81
detail.quantity,
82
detail.fees,
83
detail.discount,
84
detail.taxable_amount,
85
detail.sgst_amount,
86
detail.cgst_amount,
87
detail.igst_amount,
88
detail.total_amount,
89
detail.serial_number,
90
false -- New records are active (not deleted)
91
)
92
ON CONFLICT (id)
93
DO UPDATE
94
SET
95
product_id = EXCLUDED.product_id,
96
price = EXCLUDED.price,
97
quantity = EXCLUDED.quantity,
98
fees = EXCLUDED.fees,
99
discount = EXCLUDED.discount,
100
taxable_amount = EXCLUDED.taxable_amount,
101
sgst_amount = EXCLUDED.sgst_amount,
102
cgst_amount = EXCLUDED.cgst_amount,
103
igst_amount = EXCLUDED.igst_amount,
104
total_amount = EXCLUDED.total_amount,
105
serial_number = EXCLUDED.serial_number,
106
is_deleted = false; -- Mark as active
107
END LOOP;
108
ELSE
109
-- Raise an error if the invoice cannot be edited
110
RAISE EXCEPTION 'Invoice cannot be edited, status is greater than or equal to 3';
111
END IF;
112
END;
113
$procedure$
|
|||||
| Procedure | handle_invoice_update | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.handle_invoice_update(IN p_invoice_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_payment_term integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_round_off numeric, IN p_note text, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_modified_by uuid, IN p_invoice_details jsonb, IN p_is_draft boolean)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
detail RECORD; -- Record variable for each detail from the JSONB array
6
BEGIN
7
-- Check if it's a draft invoice (p_is_draft is true)
8
IF p_is_draft THEN
9
-- If it's a draft, update draft_invoice_headers and draft_invoice_details tables
10
UPDATE public.draft_invoice_headers
11
SET
12
company_id = p_company_id,
13
customer_id = p_customer_id,
14
currency_id = p_currency_id,
15
invoice_date = p_invoice_date,
16
payment_term = p_payment_term,
17
due_date = p_due_date,
18
total_amount = p_total_amount,
19
taxable_amount = p_taxable_amount,
20
discount = p_discount,
21
fees = p_fees,
22
sgst_amount = p_sgst_amount,
23
cgst_amount = p_cgst_amount,
24
igst_amount = p_igst_amount,
25
round_off = p_round_off,
26
note = p_note,
27
source_warehouse_id = p_source_warehouse_id,
28
destination_warehouse_id = p_destination_warehouse_id,
29
modified_on_utc = now(),
30
modified_by = p_modified_by
31
WHERE id = p_invoice_id;
32
33
-- Soft delete existing draft invoice details
34
UPDATE public.draft_invoice_details
35
SET is_deleted = true, deleted_on_utc = now()
36
WHERE invoice_header_id = p_invoice_id;
37
38
-- Insert or update new draft invoice details
39
FOR detail IN
40
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
41
id uuid,
42
product_id uuid,
43
price numeric,
44
quantity numeric,
45
fees numeric,
46
discount numeric,
47
taxable_amount numeric,
48
sgst_amount numeric,
49
cgst_amount numeric,
50
igst_amount numeric,
51
total_amount numeric,
52
serial_number integer
53
)
54
LOOP
55
INSERT INTO public.draft_invoice_details (
56
id,
57
invoice_header_id,
58
product_id,
59
price,
60
quantity,
61
fees,
62
discount,
63
taxable_amount,
64
sgst_amount,
65
cgst_amount,
66
igst_amount,
67
total_amount,
68
serial_number,
69
is_deleted
70
) VALUES (
71
detail.id,
72
p_invoice_id,
73
detail.product_id,
74
detail.price,
75
detail.quantity,
76
detail.fees,
77
detail.discount,
78
detail.taxable_amount,
79
detail.sgst_amount,
80
detail.cgst_amount,
81
detail.igst_amount,
82
detail.total_amount,
83
detail.serial_number,
84
false -- New records are active (not deleted)
85
)
86
ON CONFLICT (id)
87
DO UPDATE
88
SET
89
product_id = EXCLUDED.product_id,
90
price = EXCLUDED.price,
91
quantity = EXCLUDED.quantity,
92
fees = EXCLUDED.fees,
93
discount = EXCLUDED.discount,
94
taxable_amount = EXCLUDED.taxable_amount,
95
sgst_amount = EXCLUDED.sgst_amount,
96
cgst_amount = EXCLUDED.cgst_amount,
97
igst_amount = EXCLUDED.igst_amount,
98
total_amount = EXCLUDED.total_amount,
99
serial_number = EXCLUDED.serial_number,
100
is_deleted = false; -- Mark as active
101
END LOOP;
102
103
ELSE
104
-- If it's not a draft (p_is_draft is false), update final invoice tables
105
UPDATE public.invoice_headers
106
SET
107
company_id = p_company_id,
108
customer_id = p_customer_id,
109
currency_id = p_currency_id,
110
invoice_date = p_invoice_date,
111
payment_term = p_payment_term,
112
due_date = p_due_date,
113
total_amount = p_total_amount,
114
taxable_amount = p_taxable_amount,
115
discount = p_discount,
116
fees = p_fees,
117
sgst_amount = p_sgst_amount,
118
cgst_amount = p_cgst_amount,
119
igst_amount = p_igst_amount,
120
round_off = p_round_off,
121
note = p_note,
122
source_warehouse_id = p_source_warehouse_id,
123
destination_warehouse_id = p_destination_warehouse_id,
124
modified_on_utc = now(),
125
modified_by = p_modified_by
126
WHERE id = p_invoice_id;
127
128
-- Soft delete existing final invoice details
129
UPDATE public.invoice_details
130
SET is_deleted = true, deleted_on_utc = now()
131
WHERE invoice_header_id = p_invoice_id;
132
133
-- Insert or update new final invoice details
134
FOR detail IN
135
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
136
id uuid,
137
product_id uuid,
138
price numeric,
139
quantity numeric,
140
fees numeric,
141
discount numeric,
142
taxable_amount numeric,
143
sgst_amount numeric,
144
cgst_amount numeric,
145
igst_amount numeric,
146
total_amount numeric,
147
serial_number integer
148
)
149
LOOP
150
INSERT INTO public.invoice_details (
151
id,
152
invoice_header_id,
153
product_id,
154
price,
155
quantity,
156
fees,
157
discount,
158
taxable_amount,
159
sgst_amount,
160
cgst_amount,
161
igst_amount,
162
total_amount,
163
serial_number,
164
is_deleted
165
) VALUES (
166
detail.id,
167
p_invoice_id,
168
detail.product_id,
169
detail.price,
170
detail.quantity,
171
detail.fees,
172
detail.discount,
173
detail.taxable_amount,
174
detail.sgst_amount,
175
detail.cgst_amount,
176
detail.igst_amount,
177
detail.total_amount,
178
detail.serial_number,
179
false -- New records are active (not deleted)
180
)
181
ON CONFLICT (id)
182
DO UPDATE
183
SET
184
product_id = EXCLUDED.product_id,
185
price = EXCLUDED.price,
186
quantity = EXCLUDED.quantity,
187
fees = EXCLUDED.fees,
188
discount = EXCLUDED.discount,
189
taxable_amount = EXCLUDED.taxable_amount,
190
sgst_amount = EXCLUDED.sgst_amount,
191
cgst_amount = EXCLUDED.cgst_amount,
192
igst_amount = EXCLUDED.igst_amount,
193
total_amount = EXCLUDED.total_amount,
194
serial_number = EXCLUDED.serial_number,
195
is_deleted = false; -- Mark as active
196
END LOOP;
197
198
END IF;
199
END;
200
$procedure$
|
|||||
| Procedure | initialize_draft_invoice_header_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_draft_invoice_header_ids(IN p_default_company_id uuid, IN p_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_header_exists BOOLEAN;
6
v_max_id BIGINT;
7
v_last_seq_value BIGINT;
8
BEGIN
9
-- Check if draft invoice header IDs already exist for the new company
10
SELECT EXISTS (
11
SELECT 1
12
FROM draft_invoice_header_ids
13
WHERE company_id = p_company_id
14
) INTO v_header_exists;
15
16
IF NOT v_header_exists THEN
17
-- Get the current maximum id from the draft_invoice_header_ids table
18
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM draft_invoice_header_ids;
19
20
-- Get the last value generated by the draft_invoice_header_ids_id_seq sequence
21
SELECT last_value INTO v_last_seq_value FROM draft_invoice_header_ids_id_seq;
22
23
-- Check if the sequence needs to be advanced
24
IF v_last_seq_value <= v_max_id THEN
25
-- Advance the sequence to be one greater than the maximum id
26
PERFORM setval('draft_invoice_header_ids_id_seq', v_max_id + 1, false);
27
END IF;
28
29
-- Insert new records for the new company
30
INSERT INTO draft_invoice_header_ids (
31
id,
32
company_id,
33
fin_year,
34
invoice_prefix,
35
invoice_length,
36
last_invoice_id
37
)
38
SELECT
39
nextval('draft_invoice_header_ids_id_seq'),
40
p_company_id,
41
fin_year,
42
invoice_prefix,
43
invoice_length,
44
0
45
FROM draft_invoice_header_ids
46
WHERE company_id = p_default_company_id;
47
48
-- Optional: Log the operation
49
RAISE NOTICE 'Draft invoice header IDs initialized for company % from company %.', p_company_id, p_default_company_id;
50
ELSE
51
RAISE NOTICE 'Draft invoice header IDs already exist for company %. Skipping initialization.', p_company_id;
52
END IF;
53
END;
54
$procedure$
|
|||||
| Procedure | initialize_invoice_payment_header_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_invoice_payment_header_ids(IN old_company_id uuid, IN new_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_header_exists BOOLEAN;
6
v_max_id BIGINT;
7
v_last_seq_value BIGINT;
8
BEGIN
9
-- Check if invoice payment header IDs already exist for the new company
10
SELECT EXISTS (
11
SELECT 1
12
FROM invoice_payment_header_ids
13
WHERE company_id = new_company_id
14
) INTO v_header_exists;
15
16
IF NOT v_header_exists THEN
17
-- Get the current maximum id from the invoice_payment_header_ids table
18
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM invoice_payment_header_ids;
19
20
-- Get the last value generated by the invoice_payment_header_ids_id_seq sequence
21
SELECT last_value INTO v_last_seq_value FROM invoice_payment_header_ids_id_seq;
22
23
-- Check if the sequence needs to be advanced
24
IF v_last_seq_value <= v_max_id THEN
25
-- Advance the sequence to be one greater than the maximum id
26
PERFORM setval('invoice_payment_header_ids_id_seq', v_max_id + 1, false);
27
RAISE NOTICE 'Sequence invoice_payment_header_ids_id_seq advanced to: %', v_max_id + 1;
28
END IF;
29
30
-- Insert new records for the new company
31
INSERT INTO invoice_payment_header_ids (
32
id,
33
company_id,
34
fin_year,
35
payment_prefix,
36
payment_length,
37
last_payment_id
38
)
39
SELECT
40
nextval('invoice_payment_header_ids_id_seq'), -- Use a sequence for generating an integer ID
41
new_company_id, -- Assign the new company ID
42
fin_year, -- Copy financial year
43
payment_prefix, -- Copy payment prefix
44
payment_length, -- Copy payment length
45
0 -- Copy last payment ID
46
FROM invoice_payment_header_ids
47
WHERE company_id = old_company_id;
48
49
-- Optional: Log the operation
50
RAISE NOTICE 'Invoice payment header IDs initialized successfully for new company ID: %', new_company_id;
51
ELSE
52
RAISE NOTICE 'Invoice payment header IDs already exist for company %. Skipping initialization.', new_company_id;
53
END IF;
54
END;
55
$procedure$
|
|||||
| Procedure | initialize_organization | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_organization(IN p_id uuid, IN p_name text, IN p_company_ids 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 text, IN p_email text, IN p_created_by uuid, IN p_default_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_company_id uuid; -- Variable for iterating through company IDs
6
v_company_name text; -- Variable for iterating through company names
7
v_company_ids uuid[]; -- Array to hold parsed company IDs
8
v_company_names text[]; -- Array to hold parsed company names
9
i integer; -- Iterator for looping
10
v_organization_exists boolean;
11
v_user_exists boolean;
12
BEGIN
13
-- Check if organization already exists by ID
14
SELECT EXISTS (
15
SELECT 1 FROM public.organizations WHERE id = p_id OR (id = p_id AND name = p_name)
16
) INTO v_organization_exists;
17
18
IF v_organization_exists THEN
19
RAISE NOTICE 'Organization with ID % already exists. Skipping initialization.', p_id;
20
ELSE
21
-- Insert into organizations table
22
INSERT INTO public.organizations (
23
id,
24
name,
25
created_on_utc,
26
created_by
27
) VALUES (
28
p_id, -- Organization ID
29
p_name, -- Organization Name
30
NOW(), -- Current UTC timestamp
31
p_created_by -- Created by user
32
);
33
34
RAISE NOTICE 'Initialized organization: % with ID: %', p_name, p_id;
35
END IF;
36
37
-- Parse company IDs and names
38
v_company_ids := string_to_array(p_company_ids, ',');
39
v_company_names := string_to_array(p_company_names, ',');
40
41
-- Loop through each company and initialize
42
FOR i IN 1..array_length(v_company_ids, 1) LOOP
43
v_company_id := v_company_ids[i];
44
v_company_name := v_company_names[i];
45
46
-- Call initialize_company for each company
47
CALL public.initialize_company(
48
v_company_id, -- Company ID passed as parameter
49
p_id, -- Organization ID
50
v_company_name, -- Organization Name
51
true, -- Is Apartment
52
p_created_by, -- Created by user
53
p_default_company_id -- Old Company ID passed as parameter
54
);
55
56
END LOOP;
57
58
v_company_id := v_company_ids[1];
59
60
-- Check if user already exists by ID or Email before creating
61
SELECT EXISTS (
62
SELECT 1 FROM public.users WHERE id = p_user_id OR email = p_email
63
) INTO v_user_exists;
64
65
IF NOT v_user_exists THEN
66
-- Call create_user function to set up the user if they don't exist
67
PERFORM public.create_user(
68
p_user_id,
69
p_email,
70
p_phone_number,
71
p_user_first_name,
72
p_user_last_name,
73
p_created_by,
74
v_company_id
75
);
76
RAISE NOTICE 'Initialized user with ID: % and email: % in organization: %', p_user_id, p_email, p_name;
77
ELSE
78
UPDATE public.users
79
SET company_id = v_company_id
80
WHERE id = p_user_id OR email = p_email;
81
RAISE NOTICE 'User with ID % or email % already exists. Skipping user creation.', p_user_id, p_email;
82
END IF;
83
END;
84
$procedure$
|
|||||
| Procedure | update_bill_next_status_for_bill_header | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_bill_next_status_for_bill_header(IN p_company_id uuid, IN p_bill_ids uuid[], IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_bill RECORD;
6
v_account_id uuid;
7
v_next_status integer;
8
v_next_status_name character varying;
9
v_issue TEXT;
10
v_next_temp_id int;
11
BEGIN
12
RAISE NOTICE 'START update_bill_next_status_for_bill_header: company_id=%, bill_ids=%, modified_by=%', p_company_id, p_bill_ids, p_modified_by;
13
14
FOR v_bill IN
15
SELECT id, bill_status_id, debit_account_id
16
FROM public.bill_headers
17
WHERE id = ANY(p_bill_ids)
18
LOOP
19
RAISE NOTICE 'Processing Bill ID: %', v_bill.id;
20
21
v_account_id := v_bill.debit_account_id;
22
23
-- Get the next status from workflow
24
RAISE NOTICE 'Fetching next status for bill ID: %, company_id: %, current status: %', v_bill.id, p_company_id, v_bill.bill_status_id;
25
26
SELECT next_status INTO v_next_status
27
FROM public.bill_workflow
28
WHERE company_id = p_company_id
29
AND status = v_bill.bill_status_id
30
AND is_deleted = false
31
AND is_enabled = true
32
LIMIT 1;
33
34
IF v_next_status IS NULL THEN
35
v_issue := 'Next status not found for company and status';
36
RAISE NOTICE 'Next status not found for company % and status %', p_company_id, v_bill.bill_status_id;
37
38
INSERT INTO public.bill_approval_issue_logs(bill_id, issue, created_on_utc, created_by)
39
VALUES (v_bill.id, v_issue, now(), p_modified_by);
40
41
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_bill_next_status;
42
INSERT INTO temp_bill_next_status(id, bill_id, status, error)
43
VALUES (v_next_temp_id, v_bill.id, 'Not found', v_issue);
44
45
CONTINUE;
46
END IF;
47
48
RAISE NOTICE 'Next status for Bill ID %: %', v_bill.id, v_next_status;
49
50
-- Call `check_bill_approval_permissions` for permission check (NEW ✅)
51
PERFORM public.check_bill_approval_permissions(
52
p_company_id,
53
v_next_status,
54
p_modified_by,
55
v_account_id,
56
0
57
);
58
59
IF NOT FOUND THEN
60
v_issue := 'User is not authorized to move bill to next status';
61
RAISE NOTICE 'User % is not authorized to move Bill ID % to next status %', p_modified_by, v_bill.id, v_next_status;
62
63
INSERT INTO public.bill_approval_issue_logs(bill_id, issue, created_on_utc, created_by)
64
VALUES (v_bill.id, v_issue, now(), p_modified_by);
65
66
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_bill_next_status;
67
INSERT INTO temp_bill_next_status(id, bill_id, status, error)
68
VALUES (v_next_temp_id, v_bill.id, 'User not authorized', v_issue);
69
70
CONTINUE;
71
END IF;
72
73
-- Move to next status
74
RAISE NOTICE 'Moving Bill ID % to next status: %', v_bill.id, v_next_status;
75
76
UPDATE public.bill_headers
77
SET bill_status_id = v_next_status,
78
modified_by = p_modified_by,
79
modified_on_utc = now()
80
WHERE id = v_bill.id;
81
82
INSERT INTO public.bill_approval_logs(
83
bill_id, status_id, approved_by, approved_on, "comment",
84
created_on_utc, created_by, approval_level
85
)
86
VALUES (
87
v_bill.id, v_bill.bill_status_id, p_modified_by, now(),
88
'Approved and moved to next status',
89
now(), p_modified_by, 0
90
);
91
92
-- Get next status name for display
93
SELECT name INTO v_next_status_name
94
FROM public.bill_statuses
95
WHERE id = v_next_status;
96
97
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_bill_next_status;
98
INSERT INTO temp_bill_next_status(id, bill_id, status, error)
99
VALUES (v_next_temp_id, v_bill.id, v_next_status_name, NULL);
100
END LOOP;
101
102
RAISE NOTICE 'END update_bill_next_status_for_bill_header';
103
END
104
$procedure$
|
|||||
| Procedure | update_invoice_account_approval_user | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_invoice_account_approval_user(IN p_updates jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
update_item jsonb;
6
record_exists int;
7
v_company_id uuid;
8
v_account_id uuid;
9
v_status_id int;
10
v_user_id uuid;
11
v_modified_by uuid;
12
v_approval_level int;
13
v_company_level int;
14
BEGIN
15
-- Loop over each item in the JSON array
16
FOR update_item IN SELECT * FROM jsonb_array_elements(p_updates)
17
LOOP
18
-- Extract values
19
v_company_id := (update_item->>'CompanyId')::uuid;
20
v_account_id := (update_item->>'AccountId')::uuid;
21
v_status_id := (update_item->>'StatusId')::int;
22
v_user_id := (update_item->>'UserId')::uuid;
23
v_modified_by := (update_item->>'ModifiedBy')::uuid;
24
v_approval_level := COALESCE((update_item->>'ApprovalLevel')::int, 0);
25
26
-- Check if record exists
27
SELECT COUNT(*) INTO record_exists
28
FROM public.invoice_approval_users_account
29
WHERE id = (update_item->>'Id')::int;
30
31
IF record_exists > 0 THEN
32
-- Update existing record
33
UPDATE public.invoice_approval_users_account
34
SET
35
company_id = v_company_id,
36
account_id = v_account_id,
37
status_id = v_status_id,
38
user_id = v_user_id,
39
approval_level = v_approval_level,
40
modified_by = v_modified_by,
41
modified_on_utc = now()
42
WHERE
43
id = (update_item->>'Id')::int
44
AND is_deleted = false;
45
ELSE
46
-- Insert new record
47
INSERT INTO public.invoice_approval_users_account (
48
company_id, account_id, status_id, user_id,
49
approval_level, created_by, created_on_utc,
50
modified_by, modified_on_utc, is_deleted
51
) VALUES (
52
v_company_id, v_account_id, v_status_id, v_user_id,
53
v_approval_level, v_modified_by, now(),
54
v_modified_by, now(), false
55
);
56
END IF;
57
58
-- Get company-level approval level
59
SELECT iw.approval_level INTO v_company_level
60
FROM public.invoice_workflow iw
61
WHERE iw.company_id = v_company_id
62
AND iw.status = v_status_id
63
AND iw.is_deleted = false
64
LIMIT 1;
65
66
-- If different, upsert into invoice_account_approval_levels
67
IF v_company_level IS DISTINCT FROM v_approval_level THEN
68
IF EXISTS (
69
SELECT 1 FROM public.invoice_account_approval_levels
70
WHERE company_id = v_company_id
71
AND account_id = v_account_id
72
AND status_id = v_status_id
73
AND is_deleted = false
74
) THEN
75
UPDATE public.invoice_account_approval_levels
76
SET approval_level_required = v_approval_level,
77
modified_on_utc = now(),
78
modified_by = v_modified_by
79
WHERE company_id = v_company_id
80
AND account_id = v_account_id
81
AND status_id = v_status_id
82
AND is_deleted = false;
83
ELSE
84
INSERT INTO public.invoice_account_approval_levels (
85
company_id, account_id, status_id, approval_level_required,
86
created_on_utc, modified_on_utc, deleted_on_utc,
87
is_deleted, created_by, modified_by
88
) VALUES (
89
v_company_id, v_account_id, v_status_id, v_approval_level,
90
now(), NULL, NULL,
91
false, v_modified_by, v_modified_by
92
);
93
END IF;
94
END IF;
95
END LOOP;
96
END;
97
$procedure$
|
|||||
| Procedure | update_invoice_header | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_invoice_header(IN p_invoice_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_customer_account_id uuid, IN p_sales_account_id uuid, IN p_invoice_date date, IN p_due_date date, IN p_payment_term integer, IN p_taxable_amount numeric, IN p_cgst_amount numeric, IN p_sgst_amount numeric, IN p_igst_amount numeric, IN p_total_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_round_off numeric, IN p_currency_id integer, IN p_note character varying, IN p_invoice_status_id integer, IN p_modified_by uuid, IN p_type integer)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
UPDATE public.invoice_headers
6
SET
7
company_id = p_company_id,
8
customer_id = p_customer_id,
9
debit_account_id = p_customer_account_id,
10
credit_account_id = p_sales_account_id,
11
invoice_date = p_invoice_date,
12
due_date = p_due_date,
13
payment_term = p_payment_term,
14
taxable_amount = p_taxable_amount,
15
cgst_amount = p_cgst_amount,
16
sgst_amount = p_sgst_amount,
17
igst_amount = p_igst_amount,
18
total_amount = p_total_amount,
19
discount = p_discount,
20
fees = p_fees,
21
round_off = p_round_off,
22
currency_id = p_currency_id,
23
note = p_note,
24
invoice_status_id = p_invoice_status_id,
25
modified_by = p_modified_by,
26
modified_on_utc = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
27
type = p_type
28
WHERE id = p_invoice_id;
29
END;
30
$procedure$
|
|||||
| Procedure | via_excel_grouped_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.via_excel_grouped_invoices(IN p_group_invoice_batch_id text, IN p_grouped_invoice_template_id uuid, IN p_execution_date date, IN p_total_count integer, IN p_success_count integer, IN p_company_id uuid, IN p_invoice_unit_data jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_group_invoice_header_id UUID := gen_random_uuid();
6
v_group_invoice_number TEXT;
7
v_created_by UUID;
8
v_invoice_header_id UUID;
9
v_unit_id INTEGER;
10
v_reference_number TEXT;
11
v_invoice_data JSONB;
12
v_is_duplicate BOOLEAN;
13
v_is_detail_duplicate BOOLEAN;
14
v_success_count INTEGER;
15
BEGIN
16
-- Fetch the system user ID from the users table
17
SELECT id INTO v_created_by
18
FROM users
19
WHERE first_name = 'System'
20
LIMIT 1;
21
22
IF v_created_by IS NULL THEN
23
RAISE EXCEPTION 'System user not found in the users table';
24
END IF;
25
26
-- Generate new group invoice number
27
v_group_invoice_number := get_new_group_invoice_number(p_company_id, p_execution_date);
28
29
-- Check if the batch ID already exists in group_invoice_headers
30
SELECT EXISTS (
31
SELECT 1
32
FROM public.group_invoice_headers
33
WHERE group_invoice_template_id = p_grouped_invoice_template_id
34
AND company_id = p_company_id
35
AND group_invoice_batch_id = p_group_invoice_batch_id::text
36
) INTO v_is_duplicate;
37
38
-- Insert into group_invoice_headers if not duplicate
39
IF NOT v_is_duplicate THEN
40
INSERT INTO public.group_invoice_headers(
41
id,
42
company_id,
43
group_invoice_template_id,
44
group_invoice_number,
45
group_invoice_batch_id,
46
execution_date,
47
error_message,
48
total_count,
49
success_count,
50
created_by,
51
created_on_utc
52
)
53
VALUES (
54
v_group_invoice_header_id,
55
p_company_id,
56
p_grouped_invoice_template_id,
57
v_group_invoice_number,
58
p_group_invoice_batch_id,
59
p_execution_date,
60
'',
61
p_total_count,
62
p_success_count,
63
v_created_by,
64
NOW()
65
);
66
ELSE
67
SELECT id
68
INTO v_group_invoice_header_id
69
FROM public.group_invoice_headers
70
WHERE company_id = p_company_id
71
AND group_invoice_batch_id = p_group_invoice_batch_id::text
72
LIMIT 1;
73
END IF;
74
75
-- Loop through the JSONB data
76
FOR v_invoice_data IN SELECT * FROM jsonb_array_elements(p_invoice_unit_data)
77
LOOP
78
-- Extract values from JSONB
79
v_unit_id := (v_invoice_data->>'unit_id')::INTEGER;
80
v_reference_number := v_invoice_data->>'reference_number';
81
82
-- Fetch invoice_header_id based on invoice_number
83
SELECT id INTO v_invoice_header_id
84
FROM public.invoice_headers
85
WHERE invoice_number = (v_invoice_data->>'invoice_number')
86
AND company_id = p_company_id;
87
88
IF v_invoice_header_id IS NOT NULL THEN
89
-- Check if group_invoice_details already exists
90
SELECT EXISTS (
91
SELECT 1
92
FROM public.group_invoice_details
93
WHERE group_invoice_header_id = v_group_invoice_header_id
94
AND invoice_header_id = v_invoice_header_id
95
AND company_id = p_company_id
96
AND unit_id = v_unit_id
97
) INTO v_is_detail_duplicate;
98
99
IF NOT v_is_detail_duplicate THEN
100
-- Insert into group_invoice_details
101
INSERT INTO public.group_invoice_details(
102
id,
103
group_invoice_header_id,
104
invoice_header_id,
105
posted_on,
106
status,
107
error_message,
108
created_on_utc,
109
created_by,
110
unit_id,
111
draft_invoice_header_id,
112
company_id,
113
reference_number
114
)
115
VALUES (
116
gen_random_uuid(),
117
v_group_invoice_header_id,
118
v_invoice_header_id,
119
p_execution_date,
120
'success',
121
'',
122
NOW(),
123
v_created_by,
124
v_unit_id,
125
'00000000-0000-0000-0000-000000000000',
126
p_company_id,
127
v_reference_number
128
);
129
END IF;
130
END IF;
131
END LOOP;
132
133
-- Count successful details for this group invoice header
134
SELECT COUNT(*)
135
INTO v_success_count
136
FROM public.group_invoice_details
137
WHERE group_invoice_header_id = v_group_invoice_header_id
138
AND status = 'success';
139
140
-- Update the success_count in group_invoice_headers
141
UPDATE public.group_invoice_headers
142
SET success_count = v_success_count
143
WHERE id = v_group_invoice_header_id;
144
145
END;
146
$procedure$
|
|||||
| Procedure | purge_sales_organization_data | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.purge_sales_organization_data(IN p_organization_ids uuid[] DEFAULT NULL::uuid[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
c_retention_hours integer := 24;
6
v_cutoff_24h timestamp := now() - make_interval(hours => GREATEST(c_retention_hours, 1));
7
8
v_orgs uuid[] := '{}';
9
v_companies uuid[] := '{}';
10
11
v_customer_ids uuid[] := '{}';
12
13
v_invoice_header_ids uuid[] := '{}';
14
v_invoice_detail_ids uuid[] := '{}';
15
v_invoice_payment_header_ids uuid[] := '{}';
16
v_invoice_payment_detail_ids uuid[] := '{}';
17
v_group_invoice_header_ids uuid[] := '{}';
18
v_group_invoice_detail_ids uuid[] := '{}';
19
20
v_draft_invoice_header_ids uuid[] := '{}';
21
v_draft_invoice_detail_ids uuid[] := '{}';
22
23
v_customer_note_header_ids uuid[] := '{}';
24
v_customer_note_detail_ids uuid[] := '{}';
25
26
v_gate_pass_header_ids uuid[] := '{}';
27
v_gate_pass_detail_ids uuid[] := '{}';
28
29
v_invoice_workflow_ids uuid[] := '{}';
30
v_invoice_voucher_ids uuid[] := '{}';
31
v_penalty_config_ids uuid[] := '{}';
32
v_recurring_sales_schedule_ids uuid[] := '{}';
33
v_group_invoice_template_ids uuid[] := '{}';
34
35
BEGIN
36
-- 1) Resolve target organizations and companies
37
IF p_organization_ids IS NULL OR array_length(p_organization_ids, 1) IS NULL THEN
38
SELECT COALESCE(array_agg(id), '{}')
39
INTO v_orgs
40
FROM organizations
41
WHERE created_on_utc > '2025-05-23'
42
AND created_on_utc < (NOW() - interval '24 hours');
43
ELSE
44
v_orgs := p_organization_ids;
45
END IF;
46
47
IF v_orgs IS NULL OR array_length(v_orgs, 1) IS NULL THEN
48
RAISE NOTICE 'No organizations found for sales cleanup.';
49
RETURN;
50
END IF;
51
52
SELECT COALESCE(array_agg(id), '{}')
53
INTO v_companies
54
FROM companies
55
WHERE organization_id = ANY(v_orgs);
56
57
IF v_companies IS NULL OR array_length(v_companies, 1) IS NULL THEN
58
RAISE NOTICE 'No companies resolved for sales purge. Orgs: %', v_orgs;
59
RETURN;
60
END IF;
61
62
RAISE NOTICE 'Sales purge targets - Organizations: %; Companies: %', v_orgs, v_companies;
63
64
-- 2) Collect IDs for child→parent deletion order
65
SELECT COALESCE(array_agg(id), '{}')
66
INTO v_customer_ids
67
FROM customers
68
WHERE company_id = ANY(v_companies);
69
70
SELECT COALESCE(array_agg(id), '{}')
71
INTO v_invoice_header_ids
72
FROM invoice_headers
73
WHERE company_id = ANY(v_companies)
74
AND created_on_utc < v_cutoff_24h;
75
76
SELECT COALESCE(array_agg(id), '{}')
77
INTO v_invoice_detail_ids
78
FROM invoice_details
79
WHERE invoice_header_id = ANY(v_invoice_header_ids);
80
81
SELECT COALESCE(array_agg(id), '{}')
82
INTO v_invoice_payment_header_ids
83
FROM invoice_payment_headers
84
WHERE company_id = ANY(v_companies)
85
AND created_on_utc < v_cutoff_24h;
86
87
SELECT COALESCE(array_agg(id), '{}')
88
INTO v_invoice_payment_detail_ids
89
FROM invoice_payment_details
90
WHERE invoice_payment_header_id = ANY(v_invoice_payment_header_ids);
91
92
SELECT COALESCE(array_agg(id), '{}')
93
INTO v_group_invoice_header_ids
94
FROM group_invoice_headers
95
WHERE company_id = ANY(v_companies);
96
97
SELECT COALESCE(array_agg(id), '{}')
98
INTO v_group_invoice_detail_ids
99
FROM group_invoice_details
100
WHERE company_id = ANY(v_companies);
101
102
SELECT COALESCE(array_agg(id), '{}')
103
INTO v_draft_invoice_header_ids
104
FROM draft_invoice_headers
105
WHERE company_id = ANY(v_companies);
106
107
SELECT COALESCE(array_agg(id), '{}')
108
INTO v_draft_invoice_detail_ids
109
FROM draft_invoice_details
110
WHERE invoice_header_id = ANY(v_draft_invoice_header_ids);
111
112
SELECT COALESCE(array_agg(id), '{}')
113
INTO v_customer_note_header_ids
114
FROM customer_note_headers
115
WHERE company_id = ANY(v_companies);
116
117
SELECT COALESCE(array_agg(id), '{}')
118
INTO v_customer_note_detail_ids
119
FROM customer_note_details
120
WHERE customer_note_header_id = ANY(v_customer_note_header_ids);
121
122
SELECT COALESCE(array_agg(id), '{}')
123
INTO v_gate_pass_header_ids
124
FROM gate_pass_headers
125
WHERE company_id = ANY(v_companies);
126
127
SELECT COALESCE(array_agg(id), '{}')
128
INTO v_gate_pass_detail_ids
129
FROM gate_pass_details
130
WHERE gate_pass_header_id = ANY(v_gate_pass_header_ids);
131
132
SELECT COALESCE(array_agg(id), '{}')
133
INTO v_invoice_workflow_ids
134
FROM invoice_workflow
135
WHERE company_id = ANY(v_companies);
136
137
SELECT COALESCE(array_agg(id), '{}')
138
INTO v_invoice_voucher_ids
139
FROM invoice_voucher_ids
140
WHERE company_id = ANY(v_companies);
141
142
SELECT COALESCE(array_agg(id), '{}')
143
INTO v_penalty_config_ids
144
FROM penalty_configs
145
WHERE company_id = ANY(v_companies);
146
147
SELECT COALESCE(array_agg(id), '{}')
148
INTO v_recurring_sales_schedule_ids
149
FROM recurring_sales_schedules
150
WHERE company_id = ANY(v_companies);
151
152
SELECT COALESCE(array_agg(id), '{}')
153
INTO v_group_invoice_template_ids
154
FROM group_invoice_templates
155
WHERE company_id = ANY(v_companies);
156
157
-- 3) Purge in strict child → parent order (avoid FK violations)
158
DELETE FROM customer_note_details
159
WHERE customer_note_header_id = ANY(v_customer_note_header_ids);
160
161
DELETE FROM customer_note_headers
162
WHERE id = ANY(v_customer_note_header_ids);
163
164
DELETE FROM customer_note_work_flows
165
WHERE company_id = ANY(v_companies);
166
167
DELETE FROM group_invoice_details
168
WHERE company_id = ANY(v_companies);
169
170
DELETE FROM group_invoice_headers
171
WHERE id = ANY(v_group_invoice_header_ids);
172
173
DELETE FROM group_invoice_header_ids
174
WHERE company_id = ANY(v_companies);
175
176
DELETE FROM group_invoice_templates
177
WHERE id = ANY(v_group_invoice_template_ids);
178
179
DELETE FROM invoice_payment_details
180
WHERE invoice_payment_header_id = ANY(v_invoice_payment_header_ids);
181
182
DELETE FROM invoice_payment_headers
183
WHERE id = ANY(v_invoice_payment_header_ids);
184
185
DELETE FROM invoice_payment_header_ids
186
WHERE company_id = ANY(v_companies);
187
188
DELETE FROM invoice_details
189
WHERE invoice_header_id = ANY(v_invoice_header_ids);
190
191
DELETE FROM invoice_headers
192
WHERE id = ANY(v_invoice_header_ids);
193
194
DELETE FROM invoice_header_ids
195
WHERE company_id = ANY(v_companies);
196
197
DELETE FROM invoice_workflow
198
WHERE id = ANY(v_invoice_workflow_ids);
199
200
DELETE FROM invoice_voucher_ids
201
WHERE id = ANY(v_invoice_voucher_ids);
202
203
DELETE FROM draft_invoice_details
204
WHERE invoice_header_id = ANY(v_draft_invoice_header_ids);
205
206
DELETE FROM draft_invoice_headers
207
WHERE id = ANY(v_draft_invoice_header_ids);
208
209
DELETE FROM draft_invoice_header_ids
210
WHERE company_id = ANY(v_companies);
211
212
DELETE FROM gate_pass_details
213
WHERE gate_pass_header_id = ANY(v_gate_pass_header_ids);
214
215
DELETE FROM gate_pass_headers
216
WHERE id = ANY(v_gate_pass_header_ids);
217
218
DELETE FROM penalty_configs
219
WHERE id = ANY(v_penalty_config_ids);
220
221
DELETE FROM recurring_sales_schedules
222
WHERE id = ANY(v_recurring_sales_schedule_ids);
223
224
DELETE FROM customers
225
WHERE id = ANY(v_customer_ids);
226
227
DELETE FROM users
228
WHERE company_id = ANY(v_companies);
229
230
DELETE FROM companies
231
WHERE id = ANY(v_companies);
232
233
RAISE NOTICE 'Sales purge complete for companies: % (orgs: %).', v_companies, v_orgs;
234
235
EXCEPTION
236
WHEN OTHERS THEN
237
RAISE NOTICE 'purge_sales_organization_data failed: %', SQLERRM;
238
END;
239
$procedure$
|
|||||
| Procedure | close_resolved_delinquency_cycles | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.close_resolved_delinquency_cycles(IN p_organization_id uuid, IN p_company_ids uuid[], IN p_modified_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
CYCLE_STATUS_CLOSE CONSTANT int := 2;
6
BEGIN
7
UPDATE delinquency_cycles dc
8
SET
9
ended_on_utc = CURRENT_TIMESTAMP,
10
status_id = CYCLE_STATUS_CLOSE, -- CLOSED
11
modified_on_utc = CURRENT_TIMESTAMP,
12
modified_by = p_modified_by
13
WHERE dc.organization_id = p_organization_id
14
AND dc.ended_on_utc IS NULL
15
AND dc.is_deleted = false
16
AND NOT EXISTS (
17
SELECT 1
18
FROM invoice_headers ih
19
WHERE ih.company_id = ANY (p_company_ids)
20
AND ih.customer_id = dc.customer_id
21
AND ih.is_deleted = false
22
AND (ih.total_amount - ih.settled_amount) > 0
23
);
24
END;
25
$procedure$
|
|||||
| Procedure | schedule_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.schedule_invoice()
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
templateRecord RECORD;
6
executionDate DATE;
7
nextExecutionDate DATE;
8
newDraftInvoiceId UUID;
9
invoiceHeaderRecord RECORD;
10
invoiceDetailRecord RECORD;
11
BEGIN
12
-- Loop through all active templates in the invoice_template table
13
FOR templateRecord IN
14
SELECT *
15
FROM public.invoice_template
16
WHERE is_deleted = false
17
AND starts_from <= CURRENT_DATE
18
AND end_date >= CURRENT_DATE
19
LOOP
20
-- Initialize executionDate to the start date of the template
21
executionDate := templateRecord.starts_from;
22
23
-- Fetch data from invoice_headers or draft_invoice_headers based on invoiceHeaderId
24
SELECT * INTO invoiceHeaderRecord
25
FROM public.invoice_headers
26
WHERE id = templateRecord.invoice_header_id AND is_deleted = false;
27
28
IF NOT FOUND THEN
29
-- If not found in invoice_headers, check in draft_invoice_headers
30
SELECT * INTO invoiceHeaderRecord
31
FROM public.draft_invoice_headers
32
WHERE id = templateRecord.invoice_header_id AND is_deleted = false;
33
END IF;
34
35
-- Loop through the date range to create execution dates
36
WHILE executionDate <= templateRecord.end_date LOOP
37
-- Calculate the next execution date based on frequency
38
CASE templateRecord.frequency_cycle
39
WHEN 'daily' THEN
40
nextExecutionDate := executionDate + INTERVAL '1 day';
41
WHEN 'weekly' THEN
42
nextExecutionDate := executionDate + INTERVAL '1 week';
43
WHEN 'monthly' THEN
44
nextExecutionDate := executionDate + INTERVAL '1 month';
45
WHEN 'yearly' THEN
46
nextExecutionDate := executionDate + INTERVAL '1 year';
47
ELSE
48
RAISE EXCEPTION 'Invalid frequency cycle: %', templateRecord.frequency_cycle;
49
END CASE;
50
51
-- Create a new draft invoice
52
newDraftInvoiceId := gen_random_uuid();
53
CALL public.create_draft_invoice_header(
54
newDraftInvoiceId,
55
invoiceHeaderRecord.company_id,
56
invoiceHeaderRecord.customer_id,
57
invoiceHeaderRecord.debit_account_id,
58
invoiceHeaderRecord.credit_account_id,
59
executionDate,
60
executionDate + INTERVAL '30 days', -- Set due date
61
invoiceHeaderRecord.payment_term,
62
invoiceHeaderRecord.taxable_amount,
63
invoiceHeaderRecord.cgst_amount,
64
invoiceHeaderRecord.sgst_amount,
65
invoiceHeaderRecord.igst_amount,
66
invoiceHeaderRecord.total_amount,
67
invoiceHeaderRecord.discount,
68
invoiceHeaderRecord.fees,
69
invoiceHeaderRecord.round_off,
70
invoiceHeaderRecord.currency_id,
71
invoiceHeaderRecord.note,
72
1, -- Set initial status to 'draft'
73
invoiceHeaderRecord.created_by,
74
invoiceHeaderRecord.invoice_voucher,
75
invoiceHeaderRecord.source_warehouse_id,
76
invoiceHeaderRecord.destination_warehouse_id,
77
invoiceHeaderRecord.so_no,
78
invoiceHeaderRecord.so_date,
79
invoiceHeaderRecord.type
80
);
81
82
-- Insert related draft invoice details
83
FOR invoiceDetailRecord IN
84
SELECT * FROM public.invoice_details
85
WHERE invoice_header_id = templateRecord.invoice_header_id AND is_deleted = false
86
LOOP
87
CALL public.create_draft_invoice_detail(
88
newDraftInvoiceId,
89
invoiceDetailRecord.price,
90
invoiceDetailRecord.quantity,
91
invoiceDetailRecord.discount,
92
invoiceDetailRecord.fees,
93
invoiceDetailRecord.cgst_amount,
94
invoiceDetailRecord.igst_amount,
95
invoiceDetailRecord.sgst_amount,
96
invoiceDetailRecord.taxable_amount,
97
invoiceDetailRecord.total_amount,
98
invoiceDetailRecord.serial_number,
99
invoiceDetailRecord.product_id
100
);
101
END LOOP;
102
103
-- Insert into apartment_invoice_template_executions
104
INSERT INTO public.apartment_invoice_template_executions (
105
id, template_id, execution_date, error_message, success_count,
106
total_count, created_on_utc, created_by, is_deleted
107
)
108
VALUES (
109
gen_random_uuid(), templateRecord.id, executionDate, '', 1, 1,
110
NOW(), invoiceHeaderRecord.created_by, false
111
);
112
113
-- Update executionDate for the next iteration
114
executionDate := nextExecutionDate;
115
END LOOP;
116
END LOOP;
117
118
RAISE NOTICE 'Invoice scheduling completed successfully.';
119
END;
120
$procedure$
|
|||||
| Procedure | test_run_batches | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.test_run_batches()
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
schedule_record RECORD;
6
v_unit RECORD;
7
v_execution_id UUID;
8
v_invoice_header_id UUID;
9
v_total_amount NUMERIC;
10
v_status TEXT;
11
v_error_message TEXT;
12
v_total_count INTEGER;
13
v_success_count INTEGER;
14
v_calculation_type INTEGER;
15
v_company_id UUID;
16
v_sales_account_id UUID;
17
v_account_receivable_id UUID;
18
v_invoice_date DATE;
19
v_starts_from DATE;
20
v_end_date DATE;
21
v_due_date DATE;
22
v_payment_term INTEGER;
23
v_currency_id INTEGER;
24
v_fixed_product_id UUID;
25
v_bhk_product_id UUID;
26
v_sqft_product_id UUID;
27
v_fixed_product_rate NUMERIC;
28
v_bhk_product_rate NUMERIC;
29
v_sqft_product_rate NUMERIC;
30
v_note TEXT;
31
v_invoice_status_id INTEGER;
32
v_due_days INTEGER;
33
v_billing_cycle TEXT;
34
v_half_year INTEGER;
35
v_quarters_of_month INTEGER;
36
v_bi_month INTEGER;
37
v_day_of_week INTEGER;
38
v_day_of_month INTEGER;
39
v_month_of_year INTEGER;
40
v_time_of_day INTERVAL;
41
v_created_by UUID;
42
v_time_as_time TIME;
43
44
BEGIN
45
-- Loop through all non-deleted schedules that should run today
46
FOR schedule_record IN
47
SELECT *
48
FROM batch_schedules
49
WHERE is_deleted = FALSE
50
AND template_id IN (
51
SELECT id
52
FROM public.apartment_invoice_templates
53
WHERE is_deleted = FALSE
54
AND (starts_from <= CURRENT_DATE
55
AND (end_date IS NULL OR end_date >= CURRENT_DATE))
56
)
57
AND last_executed_at IS DISTINCT FROM CURRENT_DATE::timestamp
58
LOOP
59
-- Assign values from schedule_record
60
v_half_year := schedule_record.half_year;
61
v_quarters_of_month := schedule_record.quarters_of_month;
62
v_bi_month := schedule_record.bi_month;
63
v_day_of_week := schedule_record.day_of_week;
64
v_day_of_month := schedule_record.day_of_month;
65
v_month_of_year := schedule_record.month_of_year;
66
v_time_of_day := schedule_record.time_of_day;
67
68
-- Convert v_time_of_day interval to time by casting extracted values to integer
69
v_time_as_time := make_time(
70
EXTRACT(HOUR FROM v_time_of_day)::INTEGER,
71
EXTRACT(MINUTE FROM v_time_of_day)::INTEGER,
72
EXTRACT(SECOND FROM v_time_of_day)::INTEGER
73
);
74
75
-- Log important information before entering the IF block
76
RAISE NOTICE 'Checking if batch should run for template_id: %', schedule_record.template_id;
77
78
-- Process based on billing cycle and check if current time matches the set time
79
IF (
80
-- Daily schedules
81
schedule_record.billing_cycle = 'Daily'
82
OR (schedule_record.billing_cycle = 'Weekly' AND v_day_of_week = EXTRACT(DOW FROM CURRENT_DATE))
83
OR (schedule_record.billing_cycle = 'Monthly' AND v_day_of_month = EXTRACT(DAY FROM CURRENT_DATE) AND CURRENT_DATE <= v_end_date)
84
OR (schedule_record.billing_cycle = 'Bi-Monthly' AND v_bi_month = CEIL(EXTRACT(MONTH FROM CURRENT_DATE) / 2) AND CURRENT_DATE <= v_end_date)
85
OR (schedule_record.billing_cycle = 'Quarterly' AND v_quarters_of_month = CEIL(EXTRACT(MONTH FROM CURRENT_DATE) / 3) AND CURRENT_DATE <= v_end_date)
86
OR (schedule_record.billing_cycle = 'Half-yearly' AND v_half_year = CASE WHEN EXTRACT(MONTH FROM CURRENT_DATE) <= 6 THEN 1 ELSE 2 END AND CURRENT_DATE <= v_end_date)
87
OR (schedule_record.billing_cycle = 'Yearly' AND v_month_of_year = EXTRACT(MONTH FROM CURRENT_DATE) AND CURRENT_DATE <= v_end_date)
88
)
89
AND CURRENT_TIME >= v_time_as_time THEN
90
-- Log the execution details
91
RAISE NOTICE 'Batch should run for template_id: % at time: %', schedule_record.template_id, v_time_as_time;
92
93
-- Begin processing the batch for this template
94
v_execution_id := gen_random_uuid();
95
v_total_count := 0;
96
v_success_count := 0;
97
v_status := 'pending';
98
v_error_message := '';
99
100
-- Insert into apartment_invoice_template_execution
101
BEGIN
102
INSERT INTO public.apartment_invoice_template_executions (
103
id, template_id, execution_date, success_count, created_on_utc, created_by, total_count, error_message
104
)
105
VALUES (
106
v_execution_id, schedule_record.template_id, NOW(), v_success_count, NOW(), v_created_by, v_total_count, v_error_message
107
);
108
RAISE NOTICE 'Inserted into apartment_invoice_template_executions for template_id: % with execution_id: %', schedule_record.template_id, v_execution_id;
109
EXCEPTION
110
WHEN OTHERS THEN
111
RAISE NOTICE 'Error inserting into apartment_invoice_template_executions for template_id: % - Error: %', schedule_record.template_id, SQLERRM;
112
END;
113
114
-- Select Status from the invoice workflow
115
SELECT MIN(status)
116
INTO v_invoice_status_id
117
FROM public.invoice_workflow
118
WHERE company_id = v_company_id
119
AND is_deleted = FALSE;
120
121
-- Loop through units associated with the template
122
FOR v_unit IN
123
SELECT unit_id, bhk, sqft_area, customer_id
124
FROM public.apartment_invoice_template_units
125
WHERE template_id = schedule_record.template_id
126
LOOP
127
BEGIN
128
v_total_count := v_total_count + 1;
129
v_invoice_header_id := gen_random_uuid();
130
v_total_amount := 0;
131
132
-- Calculate the total amount based on the calculation type
133
CASE v_calculation_type
134
WHEN 1 THEN -- Fixed method
135
v_total_amount := v_fixed_product_rate;
136
WHEN 2 THEN -- BHK
137
v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
138
WHEN 3 THEN -- SFT
139
v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
140
WHEN 4 THEN -- Fixed + BHK
141
v_total_amount := v_fixed_product_rate + (v_bhk_product_rate * (v_unit.bhk)::NUMERIC);
142
WHEN 5 THEN -- Fixed + SFT
143
v_total_amount := v_fixed_product_rate + (v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC);
144
END CASE;
145
146
-- Call to create invoice header
147
CALL public.create_invoice_header(
148
v_invoice_header_id,
149
v_company_id,
150
v_unit.customer_id,
151
v_account_receivable_id,
152
v_sales_account_id,
153
v_invoice_date,
154
v_due_date, -- Now passing the calculated due_date
155
v_payment_term,
156
v_total_amount,
157
0.0,
158
0.0,
159
0.0,
160
v_total_amount,
161
0.0,
162
0.0,
163
0.0,
164
v_currency_id,
165
v_note,
166
v_invoice_status_id,
167
v_created_by,
168
'APINV',
169
'00000000-0000-0000-0000-000000000000',
170
'00000000-0000-0000-0000-000000000000',
171
(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), -- created_on_utc set to current UTC time
172
COALESCE(NULLIF('', ''), ''), -- so_no as empty string
173
v_invoice_date,
174
3, -- Apartment type
175
v_created_by
176
);
177
178
-- Log success for invoice header creation
179
RAISE NOTICE 'Created invoice header for unit_id: %', v_unit.unit_id;
180
181
EXCEPTION
182
WHEN OTHERS THEN
183
-- Handle any errors during invoice posting
184
v_status := 'failed';
185
v_error_message := COALESCE(SQLERRM, 'Unknown error');
186
RAISE NOTICE 'Error creating invoice header for unit_id: % - Error: %', v_unit.unit_id, v_error_message;
187
END;
188
END LOOP;
189
190
-- Final logging after processing template
191
RAISE NOTICE 'Processing complete for template_id: %', schedule_record.template_id;
192
ELSE
193
RAISE NOTICE 'Batch skipped for template_id: %', schedule_record.template_id;
194
END IF;
195
END LOOP;
196
END;
197
$procedure$
|
|||||
| Procedure | create_customer_note | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_customer_note(IN p_is_debit_note boolean, IN p_customer_note_header_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_note_date date, IN p_invoice_id uuid, IN p_credit_account_id uuid, IN p_debit_account_id uuid, IN p_customer_note_status_id integer, IN p_fees numeric, IN p_discount numeric, IN p_taxable_amount numeric, IN p_cgst_amount numeric, IN p_sgst_amount numeric, IN p_igst_amount numeric, IN p_round_off numeric, IN p_total_amount numeric, IN p_note text, IN p_created_by uuid, IN p_customer_note_details jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
detail JSONB;
6
BEGIN
7
-- Insert into customer_note_headers table
8
INSERT INTO public.customer_note_headers (
9
is_debit_note,
10
id,
11
company_id,
12
customer_id,
13
note_date,
14
invoice_id,
15
credit_account_id,
16
debit_account_id,
17
customer_note_status_id,
18
fees,
19
discount,
20
taxable_amount,
21
cgst_amount,
22
sgst_amount,
23
igst_amount,
24
round_off,
25
total_amount,
26
note,
27
created_on_utc,
28
created_by,
29
is_deleted
30
)
31
VALUES (
32
p_is_debit_note, -- is_debit_note
33
p_customer_note_header_id, -- id
34
p_company_id, -- company_id
35
p_customer_id, -- customer_id
36
p_note_date, -- note_date
37
p_invoice_id, -- invoice_id
38
p_credit_account_id, -- credit_account_id
39
p_debit_account_id, -- debit_account_id
40
p_customer_note_status_id, -- note_status_id
41
p_fees, -- fees
42
p_discount, -- discount
43
p_taxable_amount, -- taxable_amount
44
p_cgst_amount, -- cgst_amount
45
p_sgst_amount, -- sgst_amount
46
p_igst_amount, -- igst_amount
47
p_round_off, -- round_off
48
p_total_amount, -- total_amount
49
p_note, -- note
50
now(), -- created_on_utc
51
p_created_by, -- created_by
52
false -- is_deleted
53
);
54
55
-- Loop through each element of the JSONB array p_customer_note_details
56
FOR detail IN
57
SELECT * FROM jsonb_array_elements(p_customer_note_details)
58
LOOP
59
INSERT INTO public.customer_note_details (
60
id,
61
customer_note_header_id,
62
product_id,
63
quantity,
64
price,
65
discount,
66
fees,
67
taxable_amount,
68
cgst_amount,
69
sgst_amount,
70
igst_amount,
71
total_amount,
72
is_deleted
73
)
74
VALUES (
75
(detail->>'id')::UUID,
76
p_customer_note_header_id,
77
(detail->>'product_id')::UUID,
78
(detail->>'quantity')::INTEGER,
79
(detail->>'price')::NUMERIC,
80
(detail->>'discount')::NUMERIC,
81
(detail->>'fees')::NUMERIC,
82
(detail->>'taxable_amount')::NUMERIC,
83
(detail->>'cgst_amount')::NUMERIC,
84
(detail->>'sgst_amount')::NUMERIC,
85
(detail->>'igst_amount')::NUMERIC,
86
(detail->>'total_amount')::NUMERIC,
87
false
88
);
89
END LOOP;
90
91
RAISE NOTICE 'Customer note and details inserted successfully';
92
93
END;
94
$procedure$
|
|||||
| Procedure | run_sales_invoice_schedule | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.run_sales_invoice_schedule()
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
templateRecord RECORD;
6
executionDate DATE;
7
nextExecutionDate DATE;
8
newDraftInvoiceId UUID;
9
invoiceHeaderRecord RECORD;
10
invoiceDetailRecord RECORD;
11
BEGIN
12
-- Loop through all active templates in the run_sales_invoice_schedule table
13
FOR templateRecord IN
14
SELECT *
15
FROM public.run_sales_invoice_schedule
16
WHERE is_deleted = false
17
AND starts_from <= CURRENT_DATE
18
AND end_date >= CURRENT_DATE
19
AND schedule_status = 'active'
20
LOOP
21
-- Initialize executionDate to the start date of the template
22
executionDate := templateRecord.starts_from;
23
24
-- Fetch data from invoice_headers or draft_invoice_headers based on invoiceHeaderId
25
SELECT * INTO invoiceHeaderRecord
26
FROM public.invoice_headers
27
WHERE id = templateRecord.invoice_header_id AND is_deleted = false;
28
29
IF NOT FOUND THEN
30
-- If not found in invoice_headers, check in draft_invoice_headers
31
SELECT * INTO invoiceHeaderRecord
32
FROM public.draft_invoice_headers
33
WHERE id = templateRecord.invoice_header_id AND is_deleted = false;
34
END IF;
35
36
-- Loop through the date range to create execution dates
37
WHILE executionDate <= templateRecord.end_date LOOP
38
-- Calculate the next execution date based on frequency
39
CASE templateRecord.frequency_cycle
40
WHEN 'daily' THEN
41
nextExecutionDate := executionDate + INTERVAL '1 day';
42
WHEN 'weekly' THEN
43
nextExecutionDate := executionDate + INTERVAL '1 week';
44
WHEN 'monthly' THEN
45
nextExecutionDate := executionDate + INTERVAL '1 month';
46
WHEN 'yearly' THEN
47
nextExecutionDate := executionDate + INTERVAL '1 year';
48
ELSE
49
RAISE EXCEPTION 'Invalid frequency cycle: %', templateRecord.frequency_cycle;
50
END CASE;
51
52
-- Create a new draft invoice
53
newDraftInvoiceId := gen_random_uuid();
54
CALL public.create_draft_invoice_header(
55
newDraftInvoiceId,
56
invoiceHeaderRecord.company_id,
57
invoiceHeaderRecord.customer_id,
58
invoiceHeaderRecord.debit_account_id,
59
invoiceHeaderRecord.credit_account_id,
60
executionDate,
61
executionDate + INTERVAL '30 days', -- Set due date
62
invoiceHeaderRecord.payment_term,
63
invoiceHeaderRecord.taxable_amount,
64
invoiceHeaderRecord.cgst_amount,
65
invoiceHeaderRecord.sgst_amount,
66
invoiceHeaderRecord.igst_amount,
67
invoiceHeaderRecord.total_amount,
68
invoiceHeaderRecord.discount,
69
invoiceHeaderRecord.fees,
70
invoiceHeaderRecord.round_off,
71
invoiceHeaderRecord.currency_id,
72
invoiceHeaderRecord.note,
73
1, -- Set initial status to 'draft'
74
'Generated by System', -- Set created_by to 'Generated by System'
75
invoiceHeaderRecord.invoice_voucher,
76
invoiceHeaderRecord.source_warehouse_id,
77
invoiceHeaderRecord.destination_warehouse_id,
78
invoiceHeaderRecord.so_no,
79
invoiceHeaderRecord.so_date,
80
invoiceHeaderRecord.type
81
);
82
83
-- Insert related draft invoice details
84
FOR invoiceDetailRecord IN
85
SELECT * FROM public.invoice_details
86
WHERE invoice_header_id = templateRecord.invoice_header_id AND is_deleted = false
87
LOOP
88
CALL public.create_draft_invoice_detail(
89
newDraftInvoiceId,
90
invoiceDetailRecord.price,
91
invoiceDetailRecord.quantity,
92
invoiceDetailRecord.discount,
93
invoiceDetailRecord.fees,
94
invoiceDetailRecord.cgst_amount,
95
invoiceDetailRecord.igst_amount,
96
invoiceDetailRecord.sgst_amount,
97
invoiceDetailRecord.taxable_amount,
98
invoiceDetailRecord.total_amount,
99
invoiceDetailRecord.serial_number,
100
invoiceDetailRecord.product_id
101
);
102
END LOOP;
103
104
-- Insert into apartment_invoice_template_executions
105
INSERT INTO public.apartment_invoice_template_executions (
106
id, template_id, execution_date, error_message, success_count,
107
total_count, created_on_utc, created_by, is_deleted
108
)
109
VALUES (
110
gen_random_uuid(), templateRecord.id, executionDate, '', 1, 1,
111
NOW(), 'Generated by System', false
112
);
113
114
-- Update executionDate for the next iteration
115
executionDate := nextExecutionDate;
116
END LOOP;
117
118
-- If all iterations are completed, set schedule_status to 'completed'
119
UPDATE public.run_sales_invoice_schedule
120
SET schedule_status = 'completed'
121
WHERE id = templateRecord.id;
122
END LOOP;
123
124
-- Set schedules to 'inactive' if they are not processed
125
UPDATE public.run_sales_invoice_schedule
126
SET schedule_status = 'inactive'
127
WHERE is_deleted = false
128
AND starts_from > CURRENT_DATE
129
AND schedule_status = 'active';
130
131
RAISE NOTICE 'Invoice scheduling completed successfully.';
132
END;
133
$procedure$
|
|||||
| Procedure | create_draft_invoice_header | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_draft_invoice_header(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_invoice_date date, IN p_due_date date, IN p_payment_term integer, IN p_taxable_amount numeric, IN p_cgst_amount numeric, IN p_sgst_amount numeric, IN p_igst_amount numeric, IN p_total_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_round_off numeric, IN p_currency_id integer, IN p_note character varying, IN p_invoice_status_id integer, IN p_created_by uuid, IN p_invoice_voucher character varying, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_so_no character varying, IN p_so_date date, IN p_type integer)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
invoice_number character varying;
6
BEGIN
7
8
invoice_number := get_new_draft_invoice_number(p_company_id,p_invoice_date);
9
10
RAISE NOTICE 'Value: %', invoice_number;
11
12
INSERT INTO
13
public.draft_invoice_headers(
14
id,
15
company_id,
16
customer_id,
17
credit_account_id,
18
debit_account_id,
19
invoice_number,
20
invoice_date,
21
due_date,
22
payment_term,
23
taxable_amount,
24
cgst_amount,
25
sgst_amount,
26
igst_amount,
27
total_amount,
28
discount,
29
fees,
30
round_off,
31
currency_id,
32
note,
33
invoice_status_id,
34
created_by,
35
invoice_voucher,
36
source_warehouse_id,
37
destination_warehouse_id,
38
created_on_utc,
39
so_no,
40
so_date,
41
type,
42
current_approval_level,
43
payment_status_id)
44
VALUES (
45
p_id,
46
p_company_id,
47
p_customer_id,
48
p_debit_account_id,
49
p_credit_account_id,
50
invoice_number,
51
p_invoice_date,
52
p_due_date,
53
p_payment_term,
54
p_taxable_amount,
55
p_cgst_amount,
56
p_sgst_amount,
57
p_igst_amount,
58
p_total_amount,
59
p_discount,
60
p_fees,
61
p_round_off,
62
p_currency_id,
63
p_note,
64
p_invoice_status_id,
65
p_created_by,
66
p_invoice_voucher,
67
p_source_warehouse_id,
68
p_destination_warehouse_id,
69
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
70
p_so_no,
71
p_so_date,
72
p_type,
73
0,
74
0
75
);
76
END;
77
$procedure$
|
|||||
| Procedure | run_scheduled_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.run_scheduled_invoice(IN p_invoice_header_id uuid, IN p_draft_invoice_header_id uuid, IN p_schedule_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_system_user_id uuid;
6
v_execution_log_id uuid := gen_random_uuid();
7
v_related_schedule_id uuid;
8
v_new_draft_invoice_id uuid;
9
v_source_is_live boolean := false;
10
v_source_invoice_id uuid;
11
src_header RECORD;
12
src_detail RECORD;
13
BEGIN
14
-- ✅ 1. Find "System" user
15
SELECT u.id
16
INTO v_system_user_id
17
FROM public.users AS u
18
WHERE u.is_deleted = false
19
AND lower(u.first_name) = 'system'
20
ORDER BY u.created_on_utc NULLS LAST, u.id
21
LIMIT 1;
22
23
IF v_system_user_id IS NULL THEN
24
RAISE NOTICE 'No user with first_name="System" found; proceeding with NULL triggered_by.';
25
END IF;
26
27
-- ✅ 2. Determine which ID to use as source
28
IF p_invoice_header_id IS NOT NULL THEN
29
v_source_invoice_id := p_invoice_header_id;
30
v_source_is_live := true;
31
ELSIF p_draft_invoice_header_id IS NOT NULL THEN
32
v_source_invoice_id := p_draft_invoice_header_id;
33
v_source_is_live := false;
34
ELSE
35
RAISE NOTICE 'Both invoice_header_id and draft_invoice_header_id are NULL; aborting.';
36
RETURN;
37
END IF;
38
39
-- ✅ 3. Get related schedule_id
40
SELECT s.id
41
INTO v_related_schedule_id
42
FROM public.recurring_sales_schedules AS s
43
WHERE s.is_deleted = false
44
AND (
45
(v_source_is_live AND s.invoice_header_id = v_source_invoice_id)
46
OR (NOT v_source_is_live AND s.draft_invoice_header_id = v_source_invoice_id)
47
)
48
ORDER BY s.starts_from DESC
49
LIMIT 1;
50
51
-- ✅ 4. Fetch source header from appropriate table
52
IF v_source_is_live THEN
53
SELECT * INTO src_header
54
FROM public.invoice_headers
55
WHERE id = v_source_invoice_id
56
AND is_deleted = false;
57
ELSE
58
SELECT * INTO src_header
59
FROM public.draft_invoice_headers
60
WHERE id = v_source_invoice_id
61
AND is_deleted = false;
62
END IF;
63
64
IF NOT FOUND THEN
65
INSERT INTO public.schedule_execution_logs (
66
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
67
) VALUES (
68
v_execution_log_id, v_related_schedule_id, (p_schedule_date::date), NOW(),
69
'Failure', 'Source invoice header not found in live or draft tables.', v_system_user_id, false
70
);
71
RAISE NOTICE 'Source invoice header % not found; aborting for date %.', v_source_invoice_id, (p_schedule_date::date);
72
RETURN;
73
END IF;
74
75
-- ✅ 5. Generate new draft invoice header
76
v_new_draft_invoice_id := gen_random_uuid();
77
78
CALL public.create_draft_invoice_header(
79
v_new_draft_invoice_id,
80
src_header.company_id,
81
src_header.customer_id,
82
src_header.debit_account_id,
83
src_header.credit_account_id,
84
(p_schedule_date::date),
85
((p_schedule_date::date) + COALESCE(src_header.payment_term, 10)),
86
src_header.payment_term,
87
src_header.taxable_amount,
88
src_header.cgst_amount,
89
src_header.sgst_amount,
90
src_header.igst_amount,
91
src_header.total_amount,
92
src_header.discount,
93
src_header.fees,
94
src_header.round_off,
95
src_header.currency_id,
96
src_header.note,
97
1,
98
v_system_user_id,
99
src_header.invoice_voucher,
100
src_header.source_warehouse_id,
101
src_header.destination_warehouse_id,
102
src_header.so_no,
103
(p_schedule_date::date),
104
src_header.type
105
);
106
107
-- ✅ 6. Clone all details
108
IF v_source_is_live THEN
109
FOR src_detail IN
110
SELECT * FROM public.invoice_details
111
WHERE invoice_header_id = v_source_invoice_id
112
LOOP
113
CALL public.create_draft_invoice_detail(
114
v_new_draft_invoice_id,
115
src_detail.price, src_detail.quantity, src_detail.discount, src_detail.fees,
116
src_detail.cgst_amount, src_detail.igst_amount, src_detail.sgst_amount,
117
src_detail.taxable_amount, src_detail.total_amount,
118
src_detail.serial_number, src_detail.product_id
119
);
120
END LOOP;
121
ELSE
122
FOR src_detail IN
123
SELECT * FROM public.draft_invoice_details
124
WHERE invoice_header_id = v_source_invoice_id
125
LOOP
126
CALL public.create_draft_invoice_detail(
127
v_new_draft_invoice_id,
128
src_detail.price, src_detail.quantity, src_detail.discount, src_detail.fees,
129
src_detail.cgst_amount, src_detail.igst_amount, src_detail.sgst_amount,
130
src_detail.taxable_amount, src_detail.total_amount,
131
src_detail.serial_number, src_detail.product_id
132
);
133
END LOOP;
134
END IF;
135
136
-- ✅ 7. Log success
137
INSERT INTO public.schedule_execution_logs (
138
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
139
) VALUES (
140
v_execution_log_id, v_related_schedule_id, (p_schedule_date::date), NOW(),
141
'Success', '', v_system_user_id, false
142
);
143
144
RAISE NOTICE 'Draft invoice % created from % (live=%), schedule_id %, on %.',
145
v_new_draft_invoice_id, v_source_invoice_id, v_source_is_live, v_related_schedule_id, (p_schedule_date::date);
146
147
EXCEPTION WHEN OTHERS THEN
148
INSERT INTO public.schedule_execution_logs (
149
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
150
) VALUES (
151
COALESCE(v_execution_log_id, gen_random_uuid()), v_related_schedule_id, (p_schedule_date::date), NOW(),
152
'Failure', SQLERRM, v_system_user_id, false
153
);
154
155
RAISE NOTICE 'Error while cloning invoice % on %: %',
156
v_source_invoice_id, (p_schedule_date::date), SQLERRM;
157
END;
158
$procedure$
|
|||||
| Procedure | insert_draft_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_draft_invoice(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_discount numeric, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_payment_term integer, IN p_invoice_status_id integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_note text, IN p_round_off numeric, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_so_no text, IN p_so_date timestamp without time zone, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_invoice_voucher text, IN p_created_by uuid, IN p_invoice_details jsonb, IN p_type integer, IN p_settled_amount numeric DEFAULT 0)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
detail RECORD;
6
v_source_warehouse_id uuid;
7
v_destination_warehouse_id uuid;
8
BEGIN
9
-- Set default values for warehouses
10
v_source_warehouse_id := COALESCE(p_source_warehouse_id, NULL);
11
v_destination_warehouse_id := COALESCE(p_destination_warehouse_id, NULL);
12
13
-- Insert into the invoice header
14
CALL public.create_draft_invoice_header (
15
p_id,
16
p_company_id,
17
p_customer_id,
18
p_credit_account_id,
19
p_debit_account_id,
20
p_invoice_date::date,
21
p_due_date::date,
22
p_payment_term,
23
p_taxable_amount,
24
p_cgst_amount,
25
p_sgst_amount,
26
p_igst_amount,
27
p_total_amount,
28
p_discount,
29
p_fees,
30
p_round_off,
31
p_currency_id,
32
p_note,
33
p_invoice_status_id,
34
p_created_by,
35
p_invoice_voucher,
36
v_source_warehouse_id,
37
v_destination_warehouse_id,
38
p_so_no,
39
p_so_date::date,
40
p_type
41
);
42
43
RAISE NOTICE 'Invoice header inserted with ID: %', p_id;
44
45
-- Loop through the JSONB array of invoice details
46
FOR detail IN
47
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
48
id uuid, product_id uuid, price numeric, quantity numeric,
49
fees numeric, discount numeric, taxable_amount numeric,
50
sgst_amount numeric, cgst_amount numeric, igst_amount numeric,
51
total_amount numeric, serial_number integer
52
)
53
LOOP
54
-- Insert into invoice details
55
INSERT INTO public.draft_invoice_details (
56
id, invoice_header_id, product_id, price, quantity, fees, discount,
57
taxable_amount, sgst_amount, cgst_amount, igst_amount, total_amount,
58
serial_number, is_deleted
59
) VALUES (
60
detail.id, p_id, detail.product_id, detail.price, detail.quantity, detail.fees,
61
detail.discount, detail.taxable_amount, detail.sgst_amount, detail.cgst_amount,
62
detail.igst_amount, detail.total_amount, detail.serial_number, false
63
);
64
65
RAISE NOTICE 'Invoice detail inserted with ID: %', detail.id;
66
END LOOP;
67
68
-- No explicit COMMIT here
69
RAISE NOTICE 'Transaction completed successfully';
70
71
EXCEPTION
72
-- Handle unique violation (duplicate key)
73
WHEN unique_violation THEN
74
RAISE NOTICE 'Duplicate entry: %, rolling back', SQLERRM;
75
ROLLBACK;
76
77
-- Handle foreign key violation
78
WHEN foreign_key_violation THEN
79
RAISE NOTICE 'Foreign key violation: %, rolling back', SQLERRM;
80
ROLLBACK;
81
82
-- Catch all other exceptions
83
WHEN OTHERS THEN
84
RAISE NOTICE 'An error occurred: %, transaction rolled back', SQLERRM;
85
ROLLBACK;
86
END;
87
$procedure$
|
|||||
| Procedure | create_schedule_invoices | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_schedule_invoices(IN p_company_id uuid, IN p_frequency_cycle text, IN p_starts_from date, IN p_end_date date, IN p_created_by uuid, IN p_invoice_header_id uuid DEFAULT NULL::uuid, IN p_draft_invoice_header_id uuid DEFAULT NULL::uuid, IN p_month_of_year integer DEFAULT NULL::integer, IN p_day_of_month integer DEFAULT NULL::integer, IN p_day_of_week integer DEFAULT NULL::integer, IN p_time_of_day interval DEFAULT '00:00:00'::interval)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_recurring_schedule_id uuid := gen_random_uuid(); -- New ID for recurring_sales_schedules
6
v_schedule_status text;
7
-- normalized copies (treat all-zero GUID as NULL)
8
v_invoice_header_id uuid := NULLIF(p_invoice_header_id, '00000000-0000-0000-0000-000000000000'::uuid);
9
v_draft_invoice_header_id uuid := NULLIF(p_draft_invoice_header_id, '00000000-0000-0000-0000-000000000000'::uuid);
10
11
v_day_of_week integer := COALESCE(p_day_of_week, 0);
12
v_day_of_month integer := COALESCE(p_day_of_month, 0);
13
v_month_of_year integer := COALESCE(p_month_of_year, 0);
14
v_time_of_day interval := COALESCE(p_time_of_day, '00:00:00'::interval);
15
BEGIN
16
----------------------------------------------------------------
17
-- Validate: exactly ONE of invoice_header_id / draft_invoice_header_id
18
----------------------------------------------------------------
19
IF (v_invoice_header_id IS NULL AND v_draft_invoice_header_id IS NULL) THEN
20
RAISE EXCEPTION 'Either invoice_header_id or draft_invoice_header_id must be provided.';
21
ELSIF (v_invoice_header_id IS NOT NULL AND v_draft_invoice_header_id IS NOT NULL) THEN
22
RAISE EXCEPTION 'Provide only one of invoice_header_id or draft_invoice_header_id, not both.';
23
END IF;
24
25
-- Determine the initial schedule status
26
IF p_starts_from > CURRENT_DATE THEN
27
v_schedule_status := 'inactive';
28
ELSE
29
v_schedule_status := 'active';
30
END IF;
31
32
-- Insert into the recurring_sales_schedules table
33
INSERT INTO public.recurring_sales_schedules (
34
id, invoice_header_id, draft_invoice_header_id, company_id, frequency_cycle, schedule_status, starts_from, end_date,
35
created_on_utc, created_by, is_deleted
36
)
37
VALUES (
38
v_recurring_schedule_id, v_invoice_header_id, v_draft_invoice_header_id, p_company_id, p_frequency_cycle, v_schedule_status,
39
p_starts_from, p_end_date, NOW(), p_created_by, false
40
);
41
42
-- Insert into the public.recurrence_schedule_details table based on p_frequency_cycle
43
IF p_frequency_cycle = 'Daily' THEN
44
INSERT INTO public.recurrence_schedule_details (
45
schedule_id , frequency_cycle, time_of_day, is_deleted
46
)
47
VALUES (
48
v_recurring_schedule_id, p_frequency_cycle, v_time_of_day, false
49
);
50
51
ELSIF p_frequency_cycle = 'Weekly' THEN
52
INSERT INTO public.recurrence_schedule_details (
53
schedule_id, frequency_cycle, day_of_week, time_of_day, is_deleted
54
)
55
VALUES (
56
v_recurring_schedule_id, p_frequency_cycle, v_day_of_week, v_time_of_day, false
57
);
58
59
ELSIF p_frequency_cycle = 'Monthly' THEN
60
INSERT INTO public.recurrence_schedule_details (
61
schedule_id, frequency_cycle, day_of_month, time_of_day, is_deleted
62
)
63
VALUES (
64
v_recurring_schedule_id, p_frequency_cycle, v_day_of_month, v_time_of_day, false
65
);
66
67
ELSIF p_frequency_cycle = 'Yearly' THEN
68
INSERT INTO public.recurrence_schedule_details (
69
schedule_id, frequency_cycle, month_of_year, day_of_month, time_of_day, is_deleted
70
)
71
VALUES (
72
v_recurring_schedule_id, p_frequency_cycle, v_month_of_year, v_day_of_month, v_time_of_day, false
73
);
74
75
ELSE
76
RAISE NOTICE 'Invalid frequency cycle: %', p_frequency_cycle;
77
END IF;
78
79
RAISE NOTICE 'Recurring schedule created successfully with ID: %, Status: %', v_recurring_schedule_id, v_schedule_status;
80
END;
81
$procedure$
|
|||||
| Procedure | insert_invoice | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_invoice(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_discount numeric, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_payment_term integer, IN p_invoice_status_id integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_note text, IN p_round_off numeric, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_so_no text, IN p_so_date timestamp without time zone, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_invoice_voucher text, IN p_created_by uuid, IN p_invoice_details jsonb, IN p_type integer, IN p_settled_amount numeric DEFAULT 0)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
detail RECORD;
6
v_source_warehouse_id uuid;
7
v_destination_warehouse_id uuid;
8
BEGIN
9
-- Set default values for warehouses
10
v_source_warehouse_id := COALESCE(p_source_warehouse_id, NULL);
11
v_destination_warehouse_id := COALESCE(p_destination_warehouse_id, NULL);
12
13
-- Insert into the invoice header
14
CALL public.create_invoice_header (
15
p_id,
16
p_company_id,
17
p_customer_id,
18
p_credit_account_id,
19
p_debit_account_id,
20
p_invoice_date::date,
21
p_due_date::date,
22
p_payment_term,
23
p_taxable_amount,
24
p_cgst_amount,
25
p_sgst_amount,
26
p_igst_amount,
27
p_total_amount,
28
p_discount,
29
p_fees,
30
p_round_off,
31
p_currency_id,
32
p_note,
33
p_invoice_status_id,
34
p_created_by,
35
p_invoice_voucher,
36
v_source_warehouse_id,
37
v_destination_warehouse_id,
38
p_so_no,
39
p_so_date::date,
40
p_type
41
);
42
43
RAISE NOTICE 'Invoice header inserted with ID: %', p_id;
44
45
-- Loop through the JSONB array of invoice details
46
FOR detail IN
47
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
48
id uuid, product_id uuid, price numeric, quantity numeric,
49
fees numeric, discount numeric, taxable_amount numeric,
50
sgst_amount numeric, cgst_amount numeric, igst_amount numeric,
51
total_amount numeric, serial_number integer
52
)
53
LOOP
54
-- Insert into invoice details
55
INSERT INTO public.invoice_details (
56
id, invoice_header_id, product_id, price, quantity, fees, discount,
57
taxable_amount, sgst_amount, cgst_amount, igst_amount, total_amount,
58
serial_number, is_deleted
59
) VALUES (
60
detail.id, p_id, detail.product_id, detail.price, detail.quantity, detail.fees,
61
detail.discount, detail.taxable_amount, detail.sgst_amount, detail.cgst_amount,
62
detail.igst_amount, detail.total_amount, detail.serial_number, false
63
);
64
65
RAISE NOTICE 'Invoice detail inserted with ID: %', detail.id;
66
END LOOP;
67
68
-- No explicit COMMIT here
69
RAISE NOTICE 'Transaction completed successfully';
70
71
EXCEPTION
72
-- Handle unique violation (duplicate key)
73
WHEN unique_violation THEN
74
RAISE NOTICE 'Duplicate entry: %, rolling back', SQLERRM;
75
ROLLBACK;
76
77
-- Handle foreign key violation
78
WHEN foreign_key_violation THEN
79
RAISE NOTICE 'Foreign key violation: %, rolling back', SQLERRM;
80
ROLLBACK;
81
82
-- Catch all other exceptions
83
WHEN OTHERS THEN
84
RAISE NOTICE 'An error occurred: %, transaction rolled back', SQLERRM;
85
ROLLBACK;
86
END;
87
$procedure$
|
|||||
| Procedure | insert_multiple_invoices_by_excel | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_multiple_invoices_by_excel(IN p_invoices jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
invoice RECORD;
6
v_created_by uuid;
7
results TEXT[] := ARRAY[]::TEXT[];
8
record_status TEXT;
9
BEGIN
10
-- Optional: default created_by fallback
11
SELECT id INTO v_created_by
12
FROM users
13
WHERE first_name = 'System'
14
LIMIT 1;
15
16
FOR invoice IN
17
SELECT * FROM jsonb_to_recordset(p_invoices) AS (
18
invoice_id uuid,
19
company_id uuid,
20
customer_id uuid,
21
discount numeric,
22
currency_id integer,
23
invoice_date timestamp without time zone,
24
invoice_number text,
25
payment_term integer,
26
invoice_status_id integer,
27
due_date timestamp without time zone,
28
total_amount numeric,
29
taxable_amount numeric,
30
fees numeric,
31
sgst_amount numeric,
32
cgst_amount numeric,
33
igst_amount numeric,
34
note text,
35
round_off numeric,
36
debit_account_id uuid,
37
credit_account_id uuid,
38
so_no text,
39
so_date timestamp without time zone,
40
source_warehouse_id uuid,
41
destination_warehouse_id uuid,
42
invoice_voucher text,
43
created_by uuid,
44
lines jsonb,
45
type integer,
46
settled_amount numeric
47
)
48
LOOP
49
BEGIN
50
-- Duplicate? mark & skip
51
IF EXISTS (
52
SELECT 1
53
FROM public.invoice_headers
54
WHERE invoice_number = invoice.invoice_number
55
AND company_id = invoice.company_id
56
) THEN
57
record_status := 'duplicate';
58
results := array_append(results, invoice.invoice_number || ':' || record_status);
59
CONTINUE;
60
END IF;
61
62
-- Insert via your existing routine
63
CALL public.insert_invoice_by_excel(
64
invoice.invoice_id,
65
invoice.company_id,
66
invoice.customer_id,
67
invoice.discount,
68
invoice.currency_id,
69
invoice.invoice_date,
70
invoice.invoice_number,
71
invoice.payment_term,
72
invoice.invoice_status_id,
73
invoice.due_date,
74
invoice.total_amount,
75
invoice.taxable_amount,
76
invoice.fees,
77
invoice.sgst_amount,
78
invoice.cgst_amount,
79
invoice.igst_amount,
80
invoice.note,
81
invoice.round_off,
82
invoice.debit_account_id,
83
invoice.credit_account_id,
84
invoice.so_no,
85
invoice.so_date,
86
invoice.source_warehouse_id,
87
invoice.destination_warehouse_id,
88
invoice.invoice_voucher,
89
COALESCE(invoice.created_by, v_created_by),
90
invoice.lines::jsonb,
91
invoice.type,
92
invoice.settled_amount
93
);
94
record_status := 'success';
95
results := array_append(results, invoice.invoice_number || ':' || record_status);
96
97
EXCEPTION WHEN OTHERS THEN
98
record_status := 'failed';
99
results := array_append(results, invoice.invoice_number || ':' || record_status);
100
CONTINUE;
101
END;
102
END LOOP;
103
104
-- Emit overall status as before
105
IF array_position(results, '%:failed') IS NOT NULL THEN
106
RAISE NOTICE 'STATUS:error';
107
ELSIF array_position(results, '%:duplicate') IS NOT NULL THEN
108
RAISE NOTICE 'STATUS:duplicate';
109
ELSE
110
RAISE NOTICE 'STATUS:success';
111
END IF;
112
113
-- Emit per-record status (as a JSONB array for convenience)
114
RAISE NOTICE 'DETAILS:%', to_jsonb(results);
115
116
END;
117
$procedure$
|
|||||
| Procedure | create_invoice_payment | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_invoice_payment(IN p_invoice_payment_header_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_received_date date, IN p_mode_of_payment text, IN p_credit_account_id uuid, IN p_debit_account_id uuid, IN p_reference text, IN p_received_amount numeric, IN p_grand_total_amount numeric, IN p_advance_amount numeric, IN p_description text, IN p_tds_amount numeric, IN p_created_by uuid, IN p_invoice_payment_details jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
5
DECLARE
6
v_total_paid_amount NUMERIC;
7
v_settled_amount NUMERIC;
8
v_invoice_status_id INT;
9
v_payment_status_id INT;
10
v_invoice_header_id UUID;
11
v_payment_amount NUMERIC;
12
v_tds_amount NUMERIC;
13
v_serial_number INT;
14
v_row JSONB;
15
v_payment_number VARCHAR;
16
v_invoice_ids UUID[] := '{}';
17
BEGIN
18
v_payment_number := get_new_payment_number(p_company_id, p_received_date);
19
20
INSERT INTO public.invoice_payment_headers (
21
id,
22
company_id,
23
customer_id,
24
received_date,
25
mode_of_payment,
26
credit_account_id,
27
debit_account_id,
28
reference,
29
received_amount,
30
description,
31
tds_amount,
32
advance_amount,
33
grand_total_amount,
34
payment_number,
35
created_by,
36
created_on_utc,
37
is_deleted
38
)
39
VALUES (
40
p_invoice_payment_header_id,
41
p_company_id,
42
p_customer_id,
43
p_received_date,
44
p_mode_of_payment,
45
p_credit_account_id,
46
p_debit_account_id,
47
p_reference,
48
p_received_amount,
49
p_description,
50
p_tds_amount,
51
p_advance_amount,
52
p_grand_total_amount,
53
v_payment_number,
54
p_created_by,
55
now(),
56
false
57
);
58
59
FOR v_row IN SELECT * FROM jsonb_array_elements(p_invoice_payment_details)
60
LOOP
61
v_invoice_header_id := (v_row->>'header_id')::UUID;
62
v_payment_amount := COALESCE((v_row->>'payment_amount')::NUMERIC, 0);
63
v_tds_amount := COALESCE((v_row->>'tds_amount')::NUMERIC, 0);
64
v_serial_number := (v_row->>'serial_number')::INT;
65
v_total_paid_amount := 0;
66
v_payment_status_id := 1;
67
v_invoice_status_id := 1;
68
69
INSERT INTO public.invoice_payment_details (
70
id,
71
invoice_payment_header_id,
72
invoice_header_id,
73
received_amount,
74
tds_amount,
75
serial_number,
76
created_by,
77
created_on_utc
78
)
79
VALUES (
80
(v_row->>'id')::UUID,
81
p_invoice_payment_header_id,
82
v_invoice_header_id,
83
v_payment_amount,
84
v_tds_amount,
85
v_serial_number,
86
p_created_by,
87
now()
88
);
89
90
SELECT settled_amount INTO v_settled_amount
91
FROM public.invoice_headers
92
WHERE id = v_invoice_header_id;
93
94
v_total_paid_amount := COALESCE(v_settled_amount, 0) + v_payment_amount + v_tds_amount;
95
96
IF (SELECT total_amount FROM public.invoice_headers WHERE id = v_invoice_header_id) > v_total_paid_amount THEN
97
v_invoice_status_id := 4; -- PARTIALLY_PAID
98
v_payment_status_id := 2; -- PartiallyPaid
99
ELSE
100
v_invoice_status_id := 5; -- PAID
101
v_payment_status_id := 3; -- FullyPaid
102
END IF;
103
104
UPDATE public.invoice_headers
105
SET settled_amount = v_total_paid_amount,
106
invoice_status_id = v_invoice_status_id,
107
payment_status_id = v_payment_status_id
108
WHERE id = v_invoice_header_id;
109
110
v_invoice_ids := array_append(v_invoice_ids, v_invoice_header_id);
111
END LOOP;
112
113
IF array_length(v_invoice_ids, 1) > 0 THEN
114
CALL public.update_invoice_next_status_for_invoice_header(
115
p_company_id,
116
v_invoice_ids,
117
p_created_by
118
);
119
END IF;
120
121
RAISE NOTICE 'Invoice payment inserted successfully';
122
END;
123
$procedure$
|
|||||
| Procedure | create_invoice_payment | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_invoice_payment(IN p_invoice_payment_header_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_received_date date, IN p_mode_of_payment text, IN p_credit_account_id uuid, IN p_debit_account_id uuid, IN p_reference text, IN p_received_amount numeric, IN p_grand_total_amount numeric, IN p_advance_amount numeric, IN p_description text, IN p_tds_amount numeric, IN p_created_by uuid, IN p_payment_status_id integer, IN p_invoice_payment_details jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_total_paid_amount NUMERIC;
6
v_settled_amount NUMERIC;
7
v_invoice_status_id INT;
8
v_payment_status_id INT;
9
v_invoice_header_id UUID;
10
v_payment_amount NUMERIC;
11
v_tds_amount NUMERIC;
12
v_serial_number INT;
13
v_row JSONB;
14
v_payment_number VARCHAR;
15
v_invoice_ids UUID[] := '{}';
16
BEGIN
17
-- Generate payment number
18
v_payment_number := get_new_payment_number(p_company_id, p_received_date);
19
20
-- Insert payment header
21
INSERT INTO public.invoice_payment_headers (
22
id,
23
company_id,
24
customer_id,
25
received_date,
26
mode_of_payment,
27
credit_account_id,
28
debit_account_id,
29
reference,
30
received_amount,
31
description,
32
tds_amount,
33
advance_amount,
34
grand_total_amount,
35
payment_number,
36
created_by,
37
created_on_utc,
38
is_deleted,
39
payment_status_id
40
)
41
VALUES (
42
p_invoice_payment_header_id,
43
p_company_id,
44
p_customer_id,
45
p_received_date,
46
p_mode_of_payment,
47
p_credit_account_id,
48
p_debit_account_id,
49
p_reference,
50
p_received_amount,
51
p_description,
52
p_tds_amount,
53
p_advance_amount,
54
p_grand_total_amount,
55
v_payment_number,
56
p_created_by,
57
now(),
58
false,
59
p_payment_status_id
60
);
61
62
-- Process invoice-wise payments
63
FOR v_row IN SELECT * FROM jsonb_array_elements(p_invoice_payment_details)
64
LOOP
65
v_invoice_header_id := (v_row->>'header_id')::UUID;
66
v_payment_amount := COALESCE((v_row->>'payment_amount')::NUMERIC, 0);
67
v_tds_amount := COALESCE((v_row->>'tds_amount')::NUMERIC, 0);
68
v_serial_number := (v_row->>'serial_number')::INT;
69
70
INSERT INTO public.invoice_payment_details (
71
id,
72
invoice_payment_header_id,
73
invoice_header_id,
74
received_amount,
75
tds_amount,
76
serial_number,
77
created_by,
78
created_on_utc
79
)
80
VALUES (
81
(v_row->>'id')::UUID,
82
p_invoice_payment_header_id,
83
v_invoice_header_id,
84
v_payment_amount,
85
v_tds_amount,
86
v_serial_number,
87
p_created_by,
88
now()
89
);
90
91
SELECT settled_amount
92
INTO v_settled_amount
93
FROM public.invoice_headers
94
WHERE id = v_invoice_header_id;
95
96
v_total_paid_amount :=
97
COALESCE(v_settled_amount, 0)
98
+ v_payment_amount
99
+ v_tds_amount;
100
101
IF (SELECT total_amount FROM public.invoice_headers WHERE id = v_invoice_header_id)
102
> v_total_paid_amount THEN
103
v_invoice_status_id := 4; -- Partially Paid
104
v_payment_status_id := 2; -- PartiallyPaid
105
ELSE
106
v_invoice_status_id := 5; -- Paid
107
v_payment_status_id := 3; -- FullyPaid
108
END IF;
109
110
UPDATE public.invoice_headers
111
SET
112
settled_amount = v_total_paid_amount,
113
invoice_status_id = v_invoice_status_id,
114
payment_status_id = v_payment_status_id
115
WHERE id = v_invoice_header_id;
116
117
v_invoice_ids := array_append(v_invoice_ids, v_invoice_header_id);
118
END LOOP;
119
120
-- Move invoice to next workflow status
121
IF array_length(v_invoice_ids, 1) > 0 THEN
122
CALL public.update_invoice_next_status_for_invoice_header(
123
p_company_id,
124
v_invoice_ids,
125
p_created_by
126
);
127
END IF;
128
129
RAISE NOTICE 'Invoice payment inserted successfully';
130
END;
131
$procedure$
|
|||||
| Procedure | insert_delinquency_snapshot | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_delinquency_snapshot(IN p_organization_id uuid, IN p_customer_id uuid, IN p_cycle_id uuid, IN p_overdue_amount numeric, IN p_oldest_due_date date, IN p_days_past_due integer, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
6
-- 🛑 Guard: one snapshot per cycle per day
7
IF EXISTS (
8
SELECT 1
9
FROM delinquency_snapshots ds
10
WHERE ds.cycle_id = p_cycle_id
11
AND ds.created_on_utc::date = CURRENT_DATE
12
AND ds.is_deleted = false
13
) THEN
14
RETURN;
15
END IF;
16
17
INSERT INTO delinquency_snapshots (
18
id,
19
organization_id,
20
customer_id,
21
cycle_id,
22
overdue_amount,
23
oldest_due_date,
24
days_past_due,
25
aging_bucket,
26
last_evaluated_on,
27
created_on_utc,
28
created_by,
29
is_deleted
30
)
31
VALUES (
32
gen_random_uuid(),
33
p_organization_id,
34
p_customer_id,
35
p_cycle_id,
36
p_overdue_amount,
37
p_oldest_due_date,
38
p_days_past_due,
39
get_aging_bucket(p_days_past_due),
40
CURRENT_TIMESTAMP,
41
CURRENT_TIMESTAMP,
42
p_created_by,
43
false
44
);
45
END;
46
$procedure$
|
|||||
| View | invoice_approval_level_view | Missing in Target |
Source Script
Target Script
1
SELECT company_id,
2
account_id,
3
status_id,
4
COALESCE(( SELECT invoice_account_approval_levels.approval_level_required
5
FROM invoice_account_approval_levels
6
WHERE ((invoice_account_approval_levels.company_id = i.company_id) AND (invoice_account_approval_levels.account_id = i.account_id) AND (invoice_account_approval_levels.status_id = i.status_id) AND (invoice_account_approval_levels.is_deleted = false))
7
LIMIT 1), ( SELECT invoice_workflow.approval_level
8
FROM invoice_workflow
9
WHERE ((invoice_workflow.company_id = i.company_id) AND (invoice_workflow.status = i.status_id) AND (invoice_workflow.is_deleted = false) AND (invoice_workflow.is_enabled = true))
10
LIMIT 1)) AS approval_level_required
11
FROM ( SELECT invoice_account_approval_levels.company_id,
12
invoice_account_approval_levels.account_id,
13
invoice_account_approval_levels.status_id
14
FROM invoice_account_approval_levels
15
UNION
16
SELECT invoice_workflow.company_id,
17
NULL::uuid AS account_id,
18
invoice_workflow.status AS status_id
19
FROM invoice_workflow) i;
|
|||||
| View | vw_invoice_approval_permissions | Missing in Target |
Source Script
Target Script
1
SELECT COALESCE(iaua.company_id, iauc.company_id) AS company_id,
2
COALESCE(iaua.status_id, iauc.status_id) AS status_id,
3
COALESCE(iaua.user_id, iauc.user_id) AS user_id,
4
COALESCE(iaua.approval_level, iauc.approval_level) AS approval_level,
5
iaua.account_id,
6
iaua.approval_level AS account_approval_level,
7
iaua.status_id AS account_status_id
8
FROM (invoice_approval_user_company iauc
9
LEFT JOIN invoice_approval_users_account iaua ON (((iauc.company_id = iaua.company_id) AND (iauc.status_id = iaua.status_id))))
10
WHERE (((iaua.company_id = iauc.company_id) AND (iaua.status_id = iauc.status_id)) OR ((iaua.company_id IS NULL) AND (iauc.is_deleted = false) AND (iaua.is_deleted = false)));
|
|||||
| View | v_run_defaulter_fdw | Missing in Target |
Source Script
Target Script
1
SELECT result
2
FROM run_defaulter_fdw() run_defaulter_fdw(result);
|
-- Table: company_defaultor_configurations
Exists in source, missing in target
-- Table: draft_invoice_details
Exists in source, missing in target
-- Table: draft_invoice_header_ids
Exists in source, missing in target
-- Table: invoice_approval_user_company
Exists in source, missing in target
-- Table: invoice_payment_header_ids
Exists in source, missing in target
-- Table: invoice_penalties
Exists in source, missing in target
-- Table: invoice_status_company_configs
Exists in source, missing in target
-- Table: invoice_header_ids
Exists in source, missing in target
-- Table: penalty_processing_logs
Exists in source, missing in target
-- Table: schedule_execution_logs
Exists in source, missing in target
-- Table: customer_note_statuses
Exists in source, missing in target
-- Table: customer_upis
Exists in source, missing in target
-- Table: record_exists
Exists in source, missing in target
-- Table: temp_invoice_next_status
Exists in source, missing in target
-- Table: invoice_statuses
Exists in source, missing in target
-- Table: invoice_voucher_ids
Exists in source, missing in target
-- Table: customers
Exists in source, missing in target
-- Table: draft_invoice_headers
Exists in source, missing in target
-- Table: organizations
Exists in source, missing in target
-- Table: schema_versions
Exists in source, missing in target
-- Table: audit_queue_invoice_payment_headers
Exists in source, missing in target
-- Table: upis
Exists in source, missing in target
-- Table: users
Exists in source, missing in target
-- Table: v_created_by
Exists in source, missing in target
-- Table: v_invoice_status_id
Exists in source, missing in target
-- Table: warehouses
Exists in source, missing in target
-- Table: invoice_approval_users_account
Exists in source, missing in target
-- Table: invoice_approval_logs
Exists in source, missing in target
-- Table: addresses
Exists in source, missing in target
-- Table: audit_queue_invoice_details
Exists in source, missing in target
-- Table: customer_contacts
Exists in source, missing in target
-- Table: customer_note_headers
Exists in source, missing in target
-- Table: customers_audit
Exists in source, missing in target
-- Table: gate_pass_details
Exists in source, missing in target
-- Table: penalty_frequencies
Exists in source, missing in target
-- Table: payment_statuses
Exists in source, missing in target
-- Table: group_invoice_headers
Exists in source, missing in target
-- Table: invoice_account_approval_levels
Exists in source, missing in target
-- Table: invoice_workflow
Exists in source, missing in target
-- Table: invoice_payment_details
Exists in source, missing in target
-- Table: penalty_configs
Exists in source, missing in target
-- Table: roles
Exists in source, missing in target
-- Table: states
Exists in source, missing in target
-- Table: user_roles
Exists in source, missing in target
-- Table: audit_queue_invoice_headers
Exists in source, missing in target
-- Table: recurring_sales_schedules
Exists in source, missing in target
-- Table: invoice_payment_headers
Exists in source, missing in target
-- Table: invoice_headers
Exists in source, missing in target
-- Table: audit_queue
Exists in source, missing in target
-- Table: customer_bank_accounts
Exists in source, missing in target
-- Table: bank_accounts
Exists in source, missing in target
-- Table: contacts
Exists in source, missing in target
-- Table: batch_schedules
Exists in source, missing in target
-- Table: __EFMigrationsHistory
Exists in source, missing in target
-- Table: cities
Exists in source, missing in target
-- Table: countries
Exists in source, missing in target
-- Table: banks
Exists in source, missing in target
-- Table: companies
Exists in source, missing in target
-- Table: customer_default_accounts
Exists in source, missing in target
-- Table: customer_note_details
Exists in source, missing in target
-- Table: customer_note_work_flows
Exists in source, missing in target
-- Table: gate_pass_headers
Exists in source, missing in target
-- Table: gate_pass_statuses
Exists in source, missing in target
-- Table: delinquency_cycles
Exists in source, missing in target
-- Table: delinquency_snapshots
Exists in source, missing in target
-- Table: collection_policies
Exists in source, missing in target
-- Table: delinquency_discussion_messages
Exists in source, missing in target
-- Table: group_invoice_header_ids
Exists in source, missing in target
-- Table: group_invoice_details
Exists in source, missing in target
-- Table: group_invoice_template_customers
Exists in source, missing in target
-- Table: v_user_id
Exists in source, missing in target
-- Table: delinquency_actions
Exists in source, missing in target
-- Table: delinquency_resolution_window
Exists in source, missing in target
-- Table: delinquency_snapshot_invoices
Exists in source, missing in target
-- Table: group_invoice_templates
Exists in source, missing in target
-- Table: invoice_approval_issue_log
Exists in source, missing in target
-- Table: invoice_details
Exists in source, missing in target
-- Table: recurrence_schedule_details
Exists in source, missing in target
-- Table: audit_queue_invoice_payment_details
Exists in source, missing in target
-- Table: delinquency_action_types
Exists in source, missing in target
-- Table: delinquency_resolution_states
Exists in source, missing in target
-- Table: v_contact_number
Exists in source, missing in target
-- Function: check_invoice_approval_permissions
CREATE OR REPLACE FUNCTION public.check_invoice_approval_permissions(p_company_id uuid, p_status_id integer, p_user_id uuid, p_account_id uuid, p_approval_level integer)
RETURNS TABLE(company_id uuid, status_id integer, user_id uuid, approval_level integer, account_id uuid, account_status_id integer, permission_check text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_has_permission BOOLEAN;
BEGIN
-- First check for account-level permissions
RETURN QUERY
SELECT
iaua.company_id,
iaua.status_id,
iaua.user_id,
iaua.approval_level,
iaua.account_id,
iaua.status_id AS account_status_id,
'Account-level permission granted' AS permission_check
FROM public.invoice_approval_users_account iaua
WHERE iaua.company_id = p_company_id
AND iaua.status_id = p_status_id
AND iaua.user_id = p_user_id
AND iaua.account_id = p_account_id -- Account-level permissions check
AND iaua.approval_level = p_approval_level -- Checking the approval level
AND iaua.is_deleted = false;
-- If no rows were found in the account-level check, proceed to check for company-level permissions
IF NOT FOUND THEN
RETURN QUERY
SELECT
iac.company_id,
iac.status_id,
iac.user_id,
iac.approval_level,
NULL::uuid AS account_id, -- No account-level ID
iac.status_id AS account_status_id,
'Company-level permission granted' AS permission_check
FROM public.invoice_approval_user_company iac
WHERE iac.company_id = p_company_id
AND iac.status_id = p_status_id
AND iac.user_id = p_user_id
AND iac.approval_level = p_approval_level -- Checking the approval level
AND iac.is_deleted = false;
END IF;
END;
$function$
-- Function: delete_bulk_invoices
CREATE OR REPLACE FUNCTION public.delete_bulk_invoices(p_invoice_ids uuid[])
RETURNS TABLE(id integer, invoice_id uuid, status text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_invoice_id UUID;
v_status_id INT;
v_now TIMESTAMP := now();
row_index INT := 1;
BEGIN
FOREACH v_invoice_id IN ARRAY p_invoice_ids LOOP
-- Final Invoice check
SELECT ih.invoice_status_id INTO v_status_id
FROM invoice_headers ih
WHERE ih.id = v_invoice_id AND ih.is_deleted = false;
IF FOUND THEN
IF v_status_id IN (3, 4, 5) THEN
id := row_index;
invoice_id := v_invoice_id;
status := 'Blocked - Final Invoice Status is Approved/Paid/Partially Paid';
row_index := row_index + 1;
RETURN NEXT;
CONTINUE;
END IF;
-- Soft delete final invoice
UPDATE invoice_details idt
SET is_deleted = true,
deleted_on_utc = v_now
WHERE idt.invoice_header_id = v_invoice_id;
UPDATE invoice_headers ih
SET is_deleted = true,
deleted_on_utc = v_now
WHERE ih.id = v_invoice_id;
id := row_index;
invoice_id := v_invoice_id;
status := 'Deleted - Final Invoice';
row_index := row_index + 1;
RETURN NEXT;
CONTINUE;
END IF;
-- Draft Invoice check
SELECT dih.invoice_status_id INTO v_status_id
FROM draft_invoice_headers dih
WHERE dih.id = v_invoice_id AND dih.is_deleted = false;
IF FOUND THEN
IF v_status_id IN (3, 4, 5) THEN
id := row_index;
invoice_id := v_invoice_id;
status := 'Blocked - Draft Invoice Status is Approved/Paid/Partially Paid';
row_index := row_index + 1;
RETURN NEXT;
CONTINUE;
END IF;
-- Soft delete draft invoice
UPDATE draft_invoice_details did
SET is_deleted = true,
deleted_on_utc = v_now
WHERE did.invoice_header_id = v_invoice_id;
UPDATE draft_invoice_headers dih
SET is_deleted = true,
deleted_on_utc = v_now
WHERE dih.id = v_invoice_id;
id := row_index;
invoice_id := v_invoice_id;
status := 'Deleted - Draft Invoice';
row_index := row_index + 1;
RETURN NEXT;
CONTINUE;
END IF;
-- Not found
id := row_index;
invoice_id := v_invoice_id;
status := 'Skipped - Invoice Not Found or Already Deleted';
row_index := row_index + 1;
RETURN NEXT;
END LOOP;
END;
$function$
-- Function: generate_group_invoices_return_header_ids
CREATE OR REPLACE FUNCTION public.generate_group_invoices_return_header_ids(p_template_id uuid)
RETURNS uuid[]
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_ts timestamptz := now();
v_header_ids uuid[];
BEGIN
-- Call your existing (unchanged) procedure
CALL public.run_grouped_invoices(p_template_id);
-- Collect headers created in this invocation window
SELECT COALESCE(array_agg(h.id), '{}') INTO v_header_ids
FROM public.group_invoice_headers h
WHERE h.group_invoice_template_id = p_template_id
AND h.is_deleted = FALSE
AND h.created_on_utc >= v_start_ts; -- created_on_utc is already saved via NOW() in proc
RETURN v_header_ids;
END;
$function$
-- Function: get_all_customers
CREATE OR REPLACE FUNCTION public.get_all_customers(p_company_id uuid)
RETURNS TABLE(id uuid, name character varying, company_id uuid, billing_address_id uuid, billing_address_line1 text, billing_address_line2 text, billing_zip_code text, shipping_address_id uuid, shipping_address_line1 text, shipping_address_line2 text, shipping_zip_code text, gstin text, pan text, tan text, contacts jsonb, created_by uuid, modified_by uuid, created_by_user text, modified_by_user text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.id,
c.name,
c.company_id,
c.billing_address_id,
ba.address_line1 AS billing_address_line1,
ba.address_line2 AS billing_address_line2,
ba.zip_code AS billing_zip_code,
c.shipping_address_id,
sa.address_line1 AS shipping_address_line1,
sa.address_line2 AS shipping_address_line2,
sa.zip_code AS shipping_zip_code,
c.gstin,
c.pan,
c.tan,
-- JSON aggregation of contacts
COALESCE(jsonb_agg(
jsonb_build_object(
'contact_id', con.id,
'first_name', con.first_name,
'last_name', con.last_name,
'email', con.email,
'phone_number', con.phone_number,
'mobile_number', con.mobile_number,
'is_primary', con.is_primary
)
) FILTER (WHERE con.id IS NOT NULL), '[]'::jsonb) AS contacts,
c.created_by,
c.modified_by,
CONCAT(uc.first_name, ' ', uc.last_name) AS created_by_user,
CONCAT(um.first_name, ' ', um.last_name) AS modified_by_user
FROM
customers c
LEFT JOIN addresses ba ON c.billing_address_id = ba.id
LEFT JOIN addresses sa ON c.shipping_address_id = sa.id
LEFT JOIN customer_contacts cc ON c.id = cc.customer_id
LEFT JOIN contacts con ON cc.contact_id = con.id
LEFT JOIN users uc ON c.created_by = uc.id
LEFT JOIN users um ON c.modified_by = um.id
WHERE c.is_deleted = FALSE
AND (ba.is_deleted IS FALSE OR ba.is_deleted IS NULL)
AND (sa.is_deleted IS FALSE OR sa.is_deleted IS NULL)
AND (con.is_deleted IS FALSE OR con.is_deleted IS NULL)
AND c.company_id = p_company_id
GROUP BY c.id, ba.id, sa.id, uc.first_name, uc.last_name, um.first_name, um.last_name;
END;
$function$
-- Function: get_executed_grouped_invoice
CREATE OR REPLACE FUNCTION public.get_executed_grouped_invoice(p_company_id uuid)
RETURNS TABLE(id uuid, template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
executions.id,
executions.template_id,
executions.execution_date,
executions.error_message,
executions.success_count,
executions.total_count,
templates.invoice_description
FROM
public.apartment_invoice_template_executions executions
INNER JOIN
public.apartment_invoice_templates templates
ON
executions.template_id = templates.id
WHERE
executions.company_id = p_company_id
ORDER BY
executions.execution_date DESC;
END;
$function$
-- Function: get_group_invoice_summary
CREATE OR REPLACE FUNCTION public.get_group_invoice_summary(p_group_invoice_id uuid)
RETURNS TABLE(id uuid, group_invoice_number text, execution_date timestamp without time zone, invoice_description text, paid_count integer, paid_amount numeric, unpaid_count integer, unpaid_amount numeric, overdue_count integer, overdue_amount numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
gih.id,
gih.group_invoice_number,
gih.execution_date,
git.invoice_description,
-- Paid
COALESCE(COUNT(CASE WHEN ih.invoice_status_id = 5 THEN 1 END), 0)::int AS paid_count,
COALESCE(SUM(CASE WHEN ih.invoice_status_id = 5 THEN ih.total_amount END), 0) AS paid_amount,
-- Unpaid
COALESCE(COUNT(CASE WHEN ih.invoice_status_id IN (1,2,3,4) THEN 1 END), 0)::int AS unpaid_count,
COALESCE(SUM(CASE WHEN ih.invoice_status_id IN (1,2,3,4) THEN ih.total_amount END), 0) AS unpaid_amount,
-- Overdue
COALESCE(COUNT(CASE WHEN ih.invoice_status_id NOT IN (5) AND ih.due_date < now() THEN 1 END), 0)::int AS overdue_count,
COALESCE(SUM(CASE WHEN ih.invoice_status_id NOT IN (5) AND ih.due_date < now() THEN ih.total_amount END), 0) AS overdue_amount
FROM group_invoice_headers gih
INNER JOIN group_invoice_templates git
ON gih.group_invoice_template_id = git.id
LEFT JOIN group_invoice_details gid
ON gid.group_invoice_header_id = gih.id AND gid.is_deleted = false
LEFT JOIN invoice_headers ih
ON ih.id = gid.invoice_header_id AND ih.is_deleted = false
WHERE gih.id = p_group_invoice_id
AND gih.is_deleted = false
GROUP BY gih.id, gih.group_invoice_number, gih.execution_date, git.invoice_description;
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;
BEGIN
-- Get financial year details
SELECT fy.start_date, fy.end_date,
EXTRACT(YEAR FROM fy.start_date) INTO v_financial_year_start,
v_financial_year_end, v_finance_year_start
FROM public.finance_year fy
WHERE id = p_finance_id;
-- Set financial year end numeric value (next year)
v_finance_year_end := v_finance_year_start + 1;
-- Monthly Report
IF p_period_type = CONST_MONTHLY THEN
RETURN QUERY
WITH months AS (
SELECT
TO_CHAR(months.m, 'Mon YYYY') 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,
-- Accrued Income & Expense
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(4)) THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(5)) THEN je.amount ELSE 0 END), 0) AS total_expense,
-- Actual Income & Expense from Payments
COALESCE(SUM(CASE WHEN tr.transaction_source_type = 3 THEN je.amount ELSE 0 END), 0) AS actual_income, -- Invoice Payments
COALESCE(SUM(CASE
WHEN tr.transaction_source_type IN (4, 6)
AND at.id IN (8,9)
THEN ABS(je.amount)
ELSE 0 END), 0) AS actual_expense
-- Bill & Salary Payments
FROM
months
LEFT JOIN journal_entries je
ON EXTRACT(MONTH FROM je.transaction_date) = months.month_num
AND EXTRACT(YEAR FROM je.transaction_date) = months.extracted_year
AND je.is_deleted = FALSE
JOIN transaction_headers tr ON je.transaction_id = tr.id AND tr.company_id = p_company_id
JOIN chart_of_accounts coa ON je.account_id = coa.id
JOIN public.account_types at ON coa.account_type_id = at.id
GROUP BY months.period, months.extracted_year, months.month_num
ORDER BY months.extracted_year, months.month_num;
-- QUARTERLY REPORT
ELSEIF p_period_type = CONST_QUARTERLY THEN
RETURN QUERY
WITH quarters AS (
SELECT 'Q1' AS period, 4 AS start_month, 6 AS end_month, v_finance_year_start AS extracted_year
UNION ALL
SELECT 'Q2', 7, 9, v_finance_year_start
UNION ALL
SELECT 'Q3', 10, 12, v_finance_year_start
UNION ALL
SELECT 'Q4', 1, 3, v_finance_year_end
)
SELECT
CONCAT(quarters.period, ' ', quarters.extracted_year) AS period,
quarters.extracted_year AS year,
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(4)) THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(5)) THEN je.amount ELSE 0 END), 0) AS total_expense,
COALESCE(SUM(CASE WHEN tr.transaction_source_type = 3 THEN je.amount ELSE 0 END), 0) AS actual_income,
COALESCE(SUM(CASE WHEN tr.transaction_source_type IN (4, 6) AND at.id IN (8,9) THEN ABS(je.amount) ELSE 0 END), 0) AS actual_expense
FROM
quarters
LEFT JOIN journal_entries je
ON EXTRACT(MONTH FROM je.transaction_date) BETWEEN quarters.start_month AND quarters.end_month
AND EXTRACT(YEAR FROM je.transaction_date) = quarters.extracted_year
AND je.is_deleted = FALSE
JOIN transaction_headers tr ON je.transaction_id = tr.id AND tr.company_id = p_company_id
JOIN chart_of_accounts coa ON je.account_id = coa.id
JOIN public.account_types at ON coa.account_type_id = at.id
GROUP BY quarters.period, quarters.extracted_year
ORDER BY quarters.extracted_year, quarters.period;
ELSEIF p_period_type = CONST_HALF_YEARLY THEN
RETURN QUERY
WITH half_years AS (
-- First Half: April to September (Same Financial Year)
SELECT 'H1' AS period, 4 AS start_month, 9 AS end_month, v_finance_year_start AS extracted_year
UNION ALL
-- Second Half: October to March (Spanning Next Year)
SELECT 'H2', 10, 12, v_finance_year_start
UNION ALL
SELECT 'H2', 1, 3, v_finance_year_end
)
SELECT
CONCAT(half_years.period, ' ', half_years.extracted_year) AS period,
half_years.extracted_year AS year,
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(4)) THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(5)) THEN je.amount ELSE 0 END), 0) AS total_expense,
COALESCE(SUM(CASE WHEN tr.transaction_source_type = 3 THEN je.amount ELSE 0 END), 0) AS actual_income,
COALESCE(SUM(CASE WHEN tr.transaction_source_type IN (4, 6) AND at.id IN (8,9) THEN ABS(je.amount) ELSE 0 END), 0) AS actual_expense
FROM
half_years
LEFT JOIN journal_entries je
ON (
EXTRACT(MONTH FROM je.transaction_date) BETWEEN half_years.start_month AND half_years.end_month
AND EXTRACT(YEAR FROM je.transaction_date) = half_years.extracted_year
)
AND je.is_deleted = FALSE
JOIN transaction_headers tr ON je.transaction_id = tr.id AND tr.company_id = p_company_id
JOIN chart_of_accounts coa ON je.account_id = coa.id
JOIN public.account_types at ON coa.account_type_id = at.id
GROUP BY half_years.period, half_years.extracted_year
ORDER BY half_years.extracted_year, half_years.period;
-- Yearly Report
ELSIF p_period_type = CONST_YEARLY THEN
RETURN QUERY
SELECT
CONCAT('Year ', EXTRACT(YEAR FROM je.transaction_date)) AS period,
EXTRACT(YEAR FROM je.transaction_date) AS year,
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(4)) THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN at.id IN (SELECT id FROM get_account_type_hierarchy(5)) THEN je.amount ELSE 0 END), 0) AS total_expense,
COALESCE(SUM(CASE WHEN tr.transaction_source_type = 3 THEN je.amount ELSE 0 END), 0) AS actual_income,
COALESCE(SUM(CASE WHEN tr.transaction_source_type IN (4, 6) AND at.id IN (8,9) THEN ABS(je.amount) ELSE 0 END), 0) AS actual_expense
FROM
journal_entries je
JOIN chart_of_accounts coa ON je.account_id = coa.id
JOIN public.account_types at ON coa.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id AND tr.company_id = p_company_id
WHERE
je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
GROUP BY year
ORDER BY year;
END IF;
END;
$function$
-- Function: get_invoice_approval_level
CREATE OR REPLACE FUNCTION public.get_invoice_approval_level(p_company_id uuid, p_account_id uuid, p_status_id integer)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
v_approval_level_required integer;
BEGIN
-- Try to get the approval level from invoice_account_approval_levels
SELECT approval_level_required
INTO v_approval_level_required
FROM public.invoice_account_approval_levels
WHERE company_id = p_company_id
AND account_id = p_account_id
AND status_id = p_status_id
AND is_deleted = false
LIMIT 1;
-- If found, return the approval level from invoice_account_approval_levels
IF FOUND THEN
RETURN v_approval_level_required;
END IF;
-- Fallback to invoice_workflow if no entry found in invoice_account_approval_levels
SELECT approval_level
INTO v_approval_level_required
FROM public.invoice_workflow
WHERE company_id = p_company_id
AND status = p_status_id
AND is_deleted = false
AND is_enabled = true
LIMIT 1;
-- Return the approval level from invoice_workflow
RETURN v_approval_level_required;
END
$function$
-- Function: get_invoice_status
CREATE OR REPLACE FUNCTION public.get_invoice_status(p_invoice_id uuid)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
invoice_status_id INTEGER;
BEGIN
-- First, try to find the status in the draft_invoice_headers table
SELECT dih.invoice_status_id
INTO invoice_status_id
FROM public.draft_invoice_headers dih
WHERE dih.id = p_invoice_id;
-- If not found in draft_invoice_headers, check invoice_headers
IF invoice_status_id IS NULL THEN
SELECT ih.invoice_status_id
INTO invoice_status_id
FROM public.invoice_headers ih
WHERE ih.id = p_invoice_id;
END IF;
RETURN invoice_status_id;
END;
$function$
-- Function: get_invoice_status_company_configs
CREATE OR REPLACE FUNCTION public.get_invoice_status_company_configs(p_company_id uuid)
RETURNS TABLE(id integer, company_id uuid, status_id integer, status_name character varying, is_enabled boolean)
LANGUAGE sql
AS $function$
SELECT
c.id,
c.company_id,
c.status_id,
s.name AS status_name,
c.is_enabled
FROM public.invoice_status_company_configs c
JOIN public.invoice_statuses s ON s.id = c.status_id
WHERE c.company_id = p_company_id
AND s.is_deleted = false;
$function$
-- Function: get_new_group_invoice_number
CREATE OR REPLACE FUNCTION public.get_new_group_invoice_number(p_company_id uuid, p_date date)
RETURNS character varying
LANGUAGE plpgsql
AS $function$
DECLARE
v_finance_year integer;
new_group_invoice_id integer;
p_prefix varchar;
group_invoice_number varchar;
BEGIN
-- Determine the start year of the financial year based on p_date
IF EXTRACT(MONTH FROM p_date) >= 4 THEN
v_finance_year := EXTRACT(YEAR FROM p_date);
ELSE
v_finance_year := EXTRACT(YEAR FROM p_date) - 1;
END IF;
-- Attempt to update the last_group_invoice_id for the given company and financial year
WITH x AS (
UPDATE public.group_invoice_header_ids
SET last_group_invoice_id = last_group_invoice_id + 1
WHERE company_id = p_company_id AND fin_year = v_finance_year
RETURNING last_group_invoice_id, group_invoice_prefix
),
insert_x AS (
-- If no rows were updated, insert a new row with the default prefix 'GIN' and default invoice_length
INSERT INTO public.group_invoice_header_ids (company_id, fin_year, group_invoice_prefix, last_group_invoice_id, group_invoice_length)
SELECT p_company_id, v_finance_year, 'GIN', 1, 8 -- Default invoice_length set to 8
WHERE NOT EXISTS (SELECT 1 FROM x)
RETURNING last_group_invoice_id, group_invoice_prefix
)
-- Use COALESCE to return the correct last_group_invoice_id and group_invoice_prefix
SELECT COALESCE((SELECT last_group_invoice_id FROM x LIMIT 1), (SELECT last_group_invoice_id FROM insert_x LIMIT 1)),
COALESCE((SELECT group_invoice_prefix FROM x LIMIT 1), (SELECT group_invoice_prefix FROM insert_x LIMIT 1))
INTO new_group_invoice_id, p_prefix;
-- Concatenate the prefix and new_group_invoice_id to form the group invoice number
group_invoice_number := p_prefix || LPAD(new_group_invoice_id::text, 4, '0');
-- Return the generated group invoice number
RETURN group_invoice_number;
END;
$function$
-- Function: get_new_payment_number
CREATE OR REPLACE FUNCTION public.get_new_payment_number(p_company_id uuid, p_date date)
RETURNS character varying
LANGUAGE plpgsql
AS $function$
DECLARE
v_finance_year integer;
new_payment_id integer;
p_prefix varchar;
payment_number varchar;
BEGIN
-- Determine the start year of the financial year based on p_date
IF EXTRACT(MONTH FROM p_date) >= 4 THEN
v_finance_year := EXTRACT(YEAR FROM p_date);
ELSE
v_finance_year := EXTRACT(YEAR FROM p_date) - 1;
END IF;
-- Try to update and return existing row
WITH x AS (
UPDATE public.invoice_payment_header_ids
SET last_payment_id = last_payment_id + 1
WHERE company_id = p_company_id AND fin_year = v_finance_year
RETURNING last_payment_id, payment_prefix
),
insert_x AS (
-- If not updated, insert a default row
INSERT INTO public.invoice_payment_header_ids (
company_id, fin_year, payment_prefix, last_payment_id, payment_length
)
SELECT p_company_id, v_finance_year, 'PAY', 1, 8
WHERE NOT EXISTS (SELECT 1 FROM x)
RETURNING last_payment_id, payment_prefix
)
-- Retrieve from update or insert result
SELECT
COALESCE((SELECT last_payment_id FROM x LIMIT 1), (SELECT last_payment_id FROM insert_x LIMIT 1)),
COALESCE((SELECT payment_prefix FROM x LIMIT 1), (SELECT payment_prefix FROM insert_x LIMIT 1))
INTO new_payment_id, p_prefix;
-- Construct the final payment number (e.g., PAY0001)
payment_number := p_prefix || LPAD(new_payment_id::text, 4, '0');
RETURN payment_number;
END;
$function$
-- Function: get_payment_distributions_for_invoice
CREATE OR REPLACE FUNCTION public.get_payment_distributions_for_invoice(p_invoice_header_id uuid)
RETURNS TABLE(payment_detail_id uuid, invoice_payment_header_id uuid, payment_number text, invoice_header_id uuid, received_amount numeric, payment_created_by uuid, payment_created_by_name text, payment_created_on timestamp without time zone)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
ipd.id AS payment_detail_id,
ipd.invoice_payment_header_id,
iph.payment_number,
ipd.invoice_header_id,
ipd.received_amount,
ipd.created_by AS payment_created_by,
CONCAT(u.first_name, ' ', u.last_name) AS payment_created_by_name,
ipd.created_on_utc AS payment_created_on
FROM
public.invoice_payment_details ipd
JOIN public.invoice_payment_headers iph ON iph.id = ipd.invoice_payment_header_id
LEFT JOIN public.users u ON u.id = ipd.created_by
WHERE
ipd.invoice_header_id = p_invoice_header_id
AND ipd.is_deleted = false
ORDER BY
ipd.created_on_utc ASC;
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: 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: upsert_invoice_status_company_config
CREATE OR REPLACE FUNCTION public.upsert_invoice_status_company_config(p_company_id uuid, p_status_id integer, p_is_enabled boolean, p_user_id uuid)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
v_id INT;
BEGIN
-- Try update first (only non-deleted)
UPDATE public.invoice_status_company_configs
SET
is_enabled = p_is_enabled,
modified_on_utc = now(),
modified_by = p_user_id
WHERE company_id = p_company_id
AND status_id = p_status_id
AND is_deleted = false
RETURNING id INTO v_id;
-- If not found, insert new with is_deleted = false
IF NOT FOUND THEN
INSERT INTO public.invoice_status_company_configs (
company_id,
status_id,
is_enabled,
created_on_utc,
created_by,
is_deleted
)
VALUES (
p_company_id,
p_status_id,
p_is_enabled,
now(),
p_user_id,
false
)
RETURNING id INTO v_id;
END IF;
RETURN v_id;
END;
$function$
-- Function: update_invoice_next_status_main
CREATE OR REPLACE FUNCTION public.update_invoice_next_status_main(p_company_id uuid, p_invoice_status_id integer, p_invoice_ids uuid[], p_modified_by uuid)
RETURNS TABLE(invoice_id uuid, status_name text, error_msg text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_error_message text;
v_invoice_ids_draft uuid[];
v_invoice_ids_pending uuid[];
v_invoice_ids_other uuid[];
v_next_temp_id int;
BEGIN
RAISE NOTICE 'START update_invoice_next_status_main: company_id=%, invoice_status_id=%, modified_by=%', p_company_id, p_invoice_status_id, p_modified_by;
RAISE NOTICE 'Incoming Invoice IDs: %', p_invoice_ids;
-- Classify invoices by current status
SELECT array_agg(id) INTO v_invoice_ids_draft
FROM public.draft_invoice_headers dih
WHERE dih.id = ANY(p_invoice_ids) AND dih.invoice_status_id = 1;
SELECT array_agg(id) INTO v_invoice_ids_pending
FROM public.draft_invoice_headers dih
WHERE dih.id = ANY(p_invoice_ids) AND dih.invoice_status_id = 2;
SELECT array_agg(id) INTO v_invoice_ids_other
FROM public.invoice_headers ih
WHERE ih.id = ANY(p_invoice_ids) AND ih.invoice_status_id >= 3;
RAISE NOTICE 'Draft invoices: %', COALESCE(v_invoice_ids_draft, '{}');
RAISE NOTICE 'Pending Approval invoices: %', COALESCE(v_invoice_ids_pending, '{}');
RAISE NOTICE 'Other (Approved or higher) invoices: %', COALESCE(v_invoice_ids_other, '{}');
BEGIN
-- Direct update draft invoices to Pending Approval
IF array_length(v_invoice_ids_draft, 1) > 0 THEN
RAISE NOTICE 'Directly updating Draft invoices to Pending Approval';
UPDATE public.draft_invoice_headers
SET invoice_status_id = 2,
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = ANY(v_invoice_ids_draft);
-- Insert approval logs
INSERT INTO public.invoice_approval_logs(
invoice_id, status_id, approved_by, approved_on, "comment",
created_on_utc, created_by, approval_level
)
SELECT
dh.id, 2, p_modified_by, now(),
'Direct update from Draft to Pending Approval',
now(), p_modified_by, 0
FROM public.draft_invoice_headers dh
WHERE dh.id = ANY(v_invoice_ids_draft);
-- Generate next id for temp_invoice_next_status
SELECT COALESCE(MAX(id), 0) INTO v_next_temp_id FROM temp_invoice_next_status;
-- Insert into temp_invoice_next_status
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
SELECT
row_number() OVER () + v_next_temp_id AS id,
dh.id,
'Pending Approval',
NULL
FROM public.draft_invoice_headers dh
WHERE dh.id = ANY(v_invoice_ids_draft);
END IF;
-- Call update_draft_invoice_next_status if Pending Approval invoices found and p_invoice_status_id = 2
IF array_length(v_invoice_ids_pending, 1) > 0 AND p_invoice_status_id = 2 THEN
RAISE NOTICE 'Calling update_draft_invoice_next_status for Pending Approval invoices';
CALL public.update_draft_invoice_next_status(p_company_id, p_invoice_status_id, v_invoice_ids_pending, p_modified_by);
END IF;
-- Call update_invoice_next_status_for_invoice_header for Approved or higher invoices
IF array_length(v_invoice_ids_other, 1) > 0 THEN
RAISE NOTICE 'Calling update_invoice_next_status_for_invoice_header for Approved or higher invoices';
CALL public.update_invoice_next_status_for_invoice_header(p_company_id, p_invoice_status_id, v_invoice_ids_other, p_modified_by);
END IF;
EXCEPTION WHEN OTHERS THEN
v_error_message := SQLERRM;
RAISE NOTICE 'Exception in child procedures: %', v_error_message;
END;
-- Return rows from temp_invoice_next_status
RAISE NOTICE 'Preparing to return rows from temp_invoice_next_status';
RETURN QUERY
SELECT tinv.invoice_id, tinv.status AS status_name, tinv.error AS error_msg
FROM temp_invoice_next_status tinv;
RAISE NOTICE 'END update_invoice_next_status_main';
END;
$function$
-- Function: update_invoice_next_status_test
CREATE OR REPLACE FUNCTION public.update_invoice_next_status_test(p_company_id uuid, p_invoice_status_id integer, p_invoice_ids uuid[], p_modified_by uuid)
RETURNS TABLE(invoice_id uuid, status text, error text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_draft_invoice RECORD;
v_next_status INTEGER;
v_account_id UUID;
v_user_level INTEGER;
v_required_levels INTEGER;
v_current_level INTEGER;
v_is_account_level BOOLEAN := false;
APPROVED_STATUS CONSTANT INTEGER := 3;
SENDING_FOR_APPROVAL_STATUS CONSTANT INTEGER := 2;
BEGIN
CREATE TEMP TABLE temp_approval_results (
invoice_id uuid,
status text,
error text
) ON COMMIT DROP;
FOR v_draft_invoice IN
SELECT id, invoice_status_id, credit_account_id, current_approval_level
FROM public.draft_invoice_headers
WHERE id = ANY(p_invoice_ids)
LOOP
BEGIN
RAISE NOTICE 'Processing invoice ID: %', v_draft_invoice.id;
v_account_id := v_draft_invoice.credit_account_id;
v_next_status := p_invoice_status_id;
v_current_level := COALESCE(v_draft_invoice.current_approval_level, 1);
IF p_invoice_status_id = SENDING_FOR_APPROVAL_STATUS THEN
RAISE NOTICE 'Sending for approval...';
WITH updated AS (
UPDATE public.draft_invoice_headers
SET invoice_status_id = v_next_status,
modified_by = p_modified_by,
modified_on_utc = now(),
current_approval_level = 1
WHERE id = v_draft_invoice.id
RETURNING id
)
INSERT INTO temp_approval_results
SELECT
u.id,
'pending',
NULL
FROM updated u;
CONTINUE;
END IF;
-- Check account-level approval
SELECT approval_level INTO v_user_level
FROM public.invoice_approval_users_account
WHERE company_id = p_company_id
AND account_id = v_account_id
AND status_id = v_next_status
AND user_id = p_modified_by
AND is_deleted = false
LIMIT 1;
IF FOUND THEN
v_is_account_level := true;
ELSE
-- Check company-level approval
SELECT approval_level INTO v_user_level
FROM public.invoice_approval_user_company
WHERE company_id = p_company_id
AND status_id = v_next_status
AND user_id = p_modified_by
AND is_deleted = false
LIMIT 1;
IF NOT FOUND THEN
RAISE NOTICE 'User % is not authorized for any approval level.', p_modified_by;
INSERT INTO temp_approval_results VALUES (
v_draft_invoice.id, 'rejected', 'User not authorized for approval'
);
CONTINUE;
END IF;
END IF;
-- Get required approval levels
SELECT approval_level_required INTO v_required_levels
FROM public.invoice_account_approval_levels
WHERE company_id = p_company_id
AND account_id = v_account_id
AND status_id = v_next_status
LIMIT 1;
v_required_levels := COALESCE(v_required_levels, 1);
RAISE NOTICE 'User level: %, Required: %, Current: %', v_user_level, v_required_levels, v_current_level;
IF v_user_level <> v_current_level THEN
INSERT INTO temp_approval_results VALUES (
v_draft_invoice.id, 'rejected', FORMAT('Approval level mismatch. Expected: %, You have: %', v_current_level, v_user_level)
);
CONTINUE;
END IF;
-- If not final level, just increment approval level
IF v_current_level < v_required_levels THEN
RAISE NOTICE 'Incrementing approval level...';
UPDATE draft_invoice_headers
SET current_approval_level = v_current_level + 1,
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_draft_invoice.id;
INSERT INTO temp_approval_results VALUES (
v_draft_invoice.id, 'pending', FORMAT('Moved to level %', v_current_level + 1)
);
ELSE
-- Final approval and status change
RAISE NOTICE 'Final approval and updating status...';
UPDATE draft_invoice_headers
SET invoice_status_id = v_next_status,
current_approval_level = NULL,
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_draft_invoice.id;
CALL public.log_invoice_approval(
v_draft_invoice.id,
v_next_status,
p_modified_by
);
IF v_next_status = APPROVED_STATUS THEN
CALL public.transfer_draft_to_invoice(
v_draft_invoice.id,
p_modified_by
);
END IF;
INSERT INTO temp_approval_results VALUES (
v_draft_invoice.id, 'approved', NULL
);
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE 'Error processing invoice ID %: %', v_draft_invoice.id, SQLERRM;
INSERT INTO temp_approval_results VALUES (
v_draft_invoice.id, 'error', SQLERRM
);
END;
END LOOP;
RETURN QUERY SELECT * FROM temp_approval_results;
END;
$function$
-- Function: audit_queue_invoice_headers_trigger
CREATE OR REPLACE FUNCTION public.audit_queue_invoice_headers_trigger()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
v_changed_by uuid;
BEGIN
v_changed_by := current_setting('app.current_user_id', true)::uuid;
IF TG_OP = 'UPDATE' THEN
INSERT INTO audit_queue(table_name, row_id, old_data, new_data, changed_by)
VALUES ('invoice_headers', NEW.id, to_jsonb(OLD), to_jsonb(NEW), v_changed_by);
ELSIF TG_OP = 'INSERT' THEN
INSERT INTO audit_queue(table_name, row_id, old_data, new_data, changed_by)
VALUES ('invoice_headers', NEW.id, NULL, to_jsonb(NEW), v_changed_by);
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO audit_queue(table_name, row_id, old_data, new_data, changed_by)
VALUES ('invoice_headers', OLD.id, to_jsonb(OLD), NULL, v_changed_by);
END IF;
PERFORM pg_notify('audit_event', currval('audit_queue_audit_queue_id_seq')::text);
RETURN NEW;
END;
$function$
-- Function: edit_draft_invoice
CREATE OR REPLACE FUNCTION public.edit_draft_invoice(p_invoice_id uuid, p_company_id uuid, p_customer_id uuid, p_currency_id integer, p_invoice_date timestamp without time zone, p_payment_term integer, p_invoice_status_id integer, p_due_date timestamp without time zone, p_total_amount numeric, p_taxable_amount numeric, p_discount numeric, p_fees numeric, p_sgst_amount numeric, p_cgst_amount numeric, p_igst_amount numeric, p_note character varying, p_round_off numeric, p_source_warehouse_id uuid, p_destination_warehouse_id uuid, p_modified_by uuid, p_invoice_details jsonb)
RETURNS void
LANGUAGE plpgsql
AS $function$
BEGIN
-- Update the draft invoice header
UPDATE draft_invoice_header
SET
company_id = p_company_id,
customer_id = p_customer_id,
currency_id = p_currency_id,
invoice_date = p_invoice_date,
payment_term = p_payment_term,
invoice_status_id = p_invoice_status_id,
due_date = p_due_date,
total_amount = p_total_amount,
taxable_amount = p_taxable_amount,
discount = p_discount,
fees = p_fees,
sgst_amount = p_sgst_amount,
cgst_amount = p_cgst_amount,
igst_amount = p_igst_amount,
note = p_note,
round_off = p_round_off,
source_warehouse_id = p_source_warehouse_id,
destination_warehouse_id = p_destination_warehouse_id,
modified_by = p_modified_by,
modified_on = NOW()
WHERE id = p_invoice_id;
-- Delete existing invoice details for the draft invoice
DELETE FROM draft_invoice_detail
WHERE invoice_id = p_invoice_id;
-- Insert the new invoice details
INSERT INTO draft_invoice_detail (
invoice_id,
product_id,
quantity,
price,
taxable_amount,
tax_amount,
total_amount
)
SELECT
p_invoice_id,
(jsonb_detail->>'product_id')::uuid,
(jsonb_detail->>'quantity')::numeric,
(jsonb_detail->>'price')::numeric,
(jsonb_detail->>'taxable_amount')::numeric,
(jsonb_detail->>'tax_amount')::numeric,
(jsonb_detail->>'total_amount')::numeric
FROM jsonb_array_elements(p_invoice_details) as jsonb_detail;
-- You can add more logic as necessary
END;
$function$
-- Function: get_customer_details
CREATE OR REPLACE FUNCTION public.get_customer_details(p_customer_id uuid)
RETURNS TABLE(id uuid, name character varying, email text, mobile_number text, phone_number text, gstin text, short_name text, pan text, tan text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.id,
c.name,
ct.email,
ct.mobile_number,
ct.phone_number,
c.gstin,
c.short_name,
c.pan,
c.tan
FROM customers c
LEFT JOIN customer_contacts cc ON cc.customer_id = c.id
LEFT JOIN contacts ct ON cc.contact_id = ct.id
WHERE c.id = p_customer_id
ORDER BY ct.is_primary DESC
LIMIT 1;
END;
$function$
-- Function: check_penalty_date
CREATE OR REPLACE FUNCTION public.check_penalty_date(due_date date, penalty_check_date date)
RETURNS boolean
LANGUAGE plpgsql
AS $function$
DECLARE
first_penalty_date DATE;
adjusted_penalty_date DATE;
last_day_of_month DATE;
BEGIN
-- Calculate the first penalty date (1 day after the due date)
first_penalty_date := due_date + INTERVAL '1 day';
-- If the penalty_check_date is before the first penalty date, return FALSE
IF penalty_check_date < first_penalty_date THEN
RETURN FALSE;
END IF;
-- Calculate the last day of the month for the penalty_check_date
last_day_of_month := DATE_TRUNC('month', penalty_check_date) + INTERVAL '1 month' - INTERVAL '1 day';
-- Calculate the adjusted penalty date for the month of the penalty_check_date
IF EXTRACT(DAY FROM first_penalty_date) > EXTRACT(DAY FROM last_day_of_month) THEN
adjusted_penalty_date := last_day_of_month;
ELSE
adjusted_penalty_date := DATE_TRUNC('month', penalty_check_date) + (EXTRACT(DAY FROM first_penalty_date) - 1) * INTERVAL '1 day';
END IF;
-- Return TRUE if the penalty_check_date matches the adjusted_penalty_date
RETURN penalty_check_date = adjusted_penalty_date;
END;
$function$
-- Function: get_group_invoice_by_id
CREATE OR REPLACE FUNCTION public.get_group_invoice_by_id(p_group_invoice_id uuid)
RETURNS TABLE(id uuid, execution_date timestamp without time zone, group_invoice_number text, invoice_description text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
gih.id,
gih.execution_date,
gih.group_invoice_number,
git.invoice_description
FROM
public.group_invoice_headers gih
INNER JOIN
public.group_invoice_templates git
ON gih.group_invoice_template_id = git.id
WHERE
gih.id = p_group_invoice_id; -- Use the function parameter here
END;
$function$
-- Function: get_all_invoice_company_approvals
CREATE OR REPLACE FUNCTION public.get_all_invoice_company_approvals(p_company_id uuid)
RETURNS TABLE(id integer, status_id integer, user_id uuid, approval_level integer, required_approval_levels integer)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
MIN(ia.id) AS id,
ia.status_id,
ia.user_id,
ia.approval_level,
iw.approval_level AS required_approval_levels
FROM invoice_approval_user_company ia
JOIN invoice_workflow iw
ON ia.company_id = iw.company_id
WHERE ia.company_id = p_company_id
AND ia.is_deleted = false
AND iw.is_deleted = false
AND iw.is_enabled = TRUE
AND iw.status = 2
GROUP BY ia.status_id, ia.user_id, ia.approval_level,iw.approval_level
ORDER BY ia.status_id, ia.user_id;
END;
$function$
-- Function: soft_delete_invoice_if_no_details
CREATE OR REPLACE FUNCTION public.soft_delete_invoice_if_no_details(p_invoice_id uuid, p_modified_by uuid)
RETURNS text
LANGUAGE plpgsql
AS $function$
BEGIN
-- Final invoice case
IF EXISTS (SELECT 1 FROM invoice_headers WHERE id = p_invoice_id) THEN
IF NOT EXISTS (SELECT 1 FROM invoice_details WHERE invoice_header_id = p_invoice_id) THEN
UPDATE invoice_headers
SET
is_deleted = TRUE,
deleted_on_utc = NOW(),
modified_by = p_modified_by
WHERE id = p_invoice_id;
RETURN 'Soft-deleted from invoice_headers';
ELSE
RETURN 'Cannot delete: invoice_details exist';
END IF;
END IF;
-- Draft invoice case
IF EXISTS (SELECT 1 FROM draft_invoice_headers WHERE id = p_invoice_id) THEN
IF NOT EXISTS (SELECT 1 FROM draft_invoice_details WHERE draft_invoice_header_id = p_invoice_id) THEN
UPDATE draft_invoice_headers
SET
is_deleted = TRUE,
deleted_on_utc = NOW(),
modified_by = p_modified_by
WHERE id = p_invoice_id;
RETURN 'Soft-deleted from draft_invoice_headers';
ELSE
RETURN 'Cannot delete: draft_invoice_details exist';
END IF;
END IF;
RETURN 'Invoice not found';
END;
$function$
-- Function: upsert_invoice_workflow_and_config
CREATE OR REPLACE FUNCTION public.upsert_invoice_workflow_and_config(p_company_id uuid, p_config jsonb, p_user_id uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
item JSONB;
v_status INT;
v_next_status INT;
v_previous_status INT;
v_is_initial BOOL;
v_is_enabled BOOL;
v_approval_level INT;
BEGIN
FOR item IN SELECT * FROM jsonb_array_elements(p_config)
LOOP
v_status := (item->>'Status')::INT;
v_next_status := (item->>'NextStatus')::INT;
v_previous_status := (item->>'PreviousStatus')::INT;
v_is_initial := (item->>'IsInitial')::BOOL;
v_is_enabled := (item->>'IsEnabled')::BOOL;
v_approval_level := (item->>'ApprovalLevel')::INT;
-- UPSERT invoice_workflow
IF EXISTS (
SELECT 1 FROM invoice_workflow
WHERE company_id = p_company_id
AND status = v_status
AND next_status = v_next_status
AND previous_status = v_previous_status
AND is_deleted = false
) THEN
UPDATE invoice_workflow
SET
is_initial = v_is_initial,
is_enabled = v_is_enabled,
approval_level = v_approval_level,
modified_on_utc = now(),
modified_by = p_user_id
WHERE company_id = p_company_id
AND status = v_status
AND next_status = v_next_status
AND previous_status = v_previous_status
AND is_deleted = false;
ELSE
INSERT INTO invoice_workflow (
company_id, status, next_status, previous_status,
is_initial, created_on_utc, is_deleted, created_by,
is_enabled, approval_level
)
VALUES (
p_company_id, v_status, v_next_status, v_previous_status,
v_is_initial, now(), false, p_user_id,
v_is_enabled, v_approval_level
);
END IF;
-- UPSERT invoice_status_company_configs
INSERT INTO invoice_status_company_configs (
company_id, status_id, is_enabled,
created_on_utc, created_by, is_deleted
)
VALUES (
p_company_id, v_status, v_is_enabled,
now(), p_user_id, false
)
ON CONFLICT (company_id, status_id)
DO UPDATE SET
is_enabled = EXCLUDED.is_enabled,
modified_on_utc = now(),
modified_by = p_user_id;
END LOOP;
END;
$function$
-- Function: list_scheduled_group_invoice_template_ids
CREATE OR REPLACE FUNCTION public.list_scheduled_group_invoice_template_ids(p_run_date timestamp without time zone)
RETURNS TABLE(group_invoice_template_id uuid, company_id uuid, organization_id uuid)
LANGUAGE sql
STABLE
AS $function$
WITH params AS (
SELECT
(p_run_date)::date AS run_date,
EXTRACT(ISODOW FROM p_run_date)::int AS run_isodow,
EXTRACT(DAY FROM p_run_date)::int AS run_day,
EXTRACT(MONTH FROM p_run_date)::int AS run_month,
EXTRACT(DAY FROM (date_trunc('month', (p_run_date)::date)
+ interval '1 month - 1 day'))::int AS days_in_month
),
fy AS (
SELECT *,
CASE WHEN run_month >= 4 THEN run_month - 3 ELSE run_month + 9 END AS fy_month_index
FROM params
),
base AS (
SELECT
-- schedule fields (explicit; no bs.*)
bs.billing_cycle,
bs.day_of_week,
bs.day_of_month,
bs.bi_month,
bs.quarters_of_month,
bs.half_year,
bs.month_of_year,
bs.last_executed_at,
-- template & org linkage
git.id AS group_invoice_template_id,
c.id AS company_id,
o.id AS organization_id,
-- template window
git.starts_from,
git.end_date,
-- precomputed calendar
f.run_date,
f.run_isodow,
f.run_day,
f.days_in_month,
f.fy_month_index
FROM public.batch_schedules bs
JOIN public.group_invoice_templates git
ON git.id = bs.group_invoice_template_id
AND git.is_deleted = FALSE
AND git.active_status = TRUE
JOIN public.companies c
ON c.id = git.company_id
AND c.is_deleted = FALSE
JOIN public.organizations o
ON o.id = c.organization_id
AND o.is_deleted = FALSE
CROSS JOIN fy f
WHERE (bs.last_executed_at IS NULL OR bs.last_executed_at::date <> f.run_date)
AND (git.starts_from IS NULL OR f.run_date >= git.starts_from::date)
AND (git.end_date IS NULL OR f.run_date <= git.end_date::date)
),
calc AS (
SELECT
b.group_invoice_template_id,
b.company_id,
b.organization_id,
CASE b.billing_cycle
WHEN 'Daily' THEN TRUE
WHEN 'Weekly' THEN COALESCE(b.day_of_week, b.run_isodow) = b.run_isodow
WHEN 'Monthly' THEN b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
WHEN 'Bi-Monthly' THEN
COALESCE(b.bi_month, 1) IN (1,2)
AND (
(b.bi_month = 1 AND (b.fy_month_index % 2) = 1) OR
(b.bi_month = 2 AND (b.fy_month_index % 2) = 0)
)
AND b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
WHEN 'Quarterly' THEN
COALESCE(b.quarters_of_month, 1) BETWEEN 1 AND 4
AND b.fy_month_index = (CASE b.quarters_of_month
WHEN 1 THEN 1
WHEN 2 THEN 4
WHEN 3 THEN 7
ELSE 10 END)
AND b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
WHEN 'Half-yearly' THEN
COALESCE(b.half_year, 1) IN (1,2)
AND b.fy_month_index = (CASE b.half_year WHEN 1 THEN 1 ELSE 7 END)
AND b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
WHEN 'Yearly' THEN
COALESCE(b.month_of_year, b.fy_month_index) = b.fy_month_index
AND b.run_day = LEAST(COALESCE(b.day_of_month, 1), b.days_in_month)
ELSE FALSE
END AS is_due
FROM base b
)
SELECT
group_invoice_template_id,
company_id,
organization_id
FROM calc
WHERE is_due = TRUE
ORDER BY organization_id, company_id, group_invoice_template_id;
$function$
-- Function: get_grouped_invoice
CREATE OR REPLACE FUNCTION public.get_grouped_invoice(p_company_id uuid)
RETURNS TABLE(id uuid, template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
executions.id,
executions.template_id,
executions.execution_date,
executions.error_message,
executions.success_count,
executions.total_count,
templates.invoice_description
FROM
public.apartment_invoice_template_executions executions
INNER JOIN
public.apartment_invoice_templates templates
ON
executions.template_id = templates.id
WHERE
executions.company_id = p_company_id
ORDER BY
executions.execution_date DESC;
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_grouped_invoice_header
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_header(p_company_id uuid)
RETURNS TABLE(id uuid, group_invoice_number text, grouped_invoice_template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text, group_invoice_batch_id text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
gih.id,
gih.group_invoice_number,
gih.group_invoice_template_id AS grouped_invoice_template_id,
gih.execution_date,
gih.error_message,
gih.success_count,
gih.total_count,
templates.invoice_description,
gih.group_invoice_batch_id
FROM
public.group_invoice_headers gih
INNER JOIN
public.group_invoice_templates templates
ON
gih.group_invoice_template_id = templates.id
WHERE
gih.company_id = p_company_id
ORDER BY
gih.execution_date DESC;
END;
$function$
-- Function: get_invoice_header_ids
CREATE OR REPLACE FUNCTION public.get_invoice_header_ids(p_company_id uuid, p_fin_year integer)
RETURNS TABLE(company_id uuid, fin_year integer, draft_invoice_prefix text, draft_invoice_length integer, invoice_prefix text, invoice_length integer, payment_prefix text, payment_length integer)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
COALESCE(draft.company_id, invoice.company_id, payment.company_id) AS company_id,
COALESCE(draft.fin_year, invoice.fin_year, payment.fin_year) AS fin_year,
draft.invoice_prefix AS draft_invoice_prefix,
draft.invoice_length AS draft_invoice_length,
invoice.invoice_prefix AS invoice_prefix,
invoice.invoice_length AS invoice_length,
payment.payment_prefix AS payment_prefix,
payment.payment_length AS payment_length
FROM
(SELECT d.company_id, d.fin_year, d.invoice_prefix, d.invoice_length FROM draft_invoice_header_ids as d) AS draft
FULL OUTER JOIN (SELECT i.company_id, i.fin_year, i.invoice_prefix, i.invoice_length FROM invoice_header_ids as i) AS invoice
ON draft.company_id = invoice.company_id AND draft.fin_year = invoice.fin_year
FULL OUTER JOIN (SELECT p.company_id, p.fin_year, p.payment_prefix, p.payment_length FROM invoice_payment_header_ids as p) AS payment
ON draft.company_id = payment.company_id AND draft.fin_year = payment.fin_year
OR invoice.company_id = payment.company_id AND invoice.fin_year = payment.fin_year
WHERE
COALESCE(draft.company_id, invoice.company_id, payment.company_id) = p_company_id
AND COALESCE(draft.fin_year, invoice.fin_year, payment.fin_year) = p_fin_year;
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, debit numeric, credit numeric, closing_balance numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
finYearStartDate DATE;
finYearLastDate DATE;
v_start_date DATE;
v_end_date DATE;
BEGIN
-- Compute Financial Year Start & End Dates
finYearStartDate := MAKE_DATE(p_fin_year_id, 4, 1); -- April 1st of the financial year
finYearLastDate := MAKE_DATE(p_fin_year_id + 1, 3, 31); -- March 31st of next year
-- Override with provided start_date and end_date if available
v_start_date := COALESCE(p_start_date, finYearStartDate);
v_end_date := COALESCE(p_end_date, finYearLastDate);
RETURN QUERY
WITH trial_balance AS (
SELECT
je.account_id,
je.account_name AS ledger,
je.account_category AS general_ledger,
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
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, je.account_name, je.account_category
),
opening_balances AS (
SELECT
je.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 < v_start_date
GROUP BY je.account_id
)
SELECT
ROW_NUMBER() OVER()::INTEGER AS sl_no,
tb.ledger,
tb.general_ledger,
COALESCE(ob.opening_balance, 0) AS opening_balance,
COALESCE(tb.total_debits, 0) AS debit,
COALESCE(tb.total_credits, 0) AS credit,
(COALESCE(ob.opening_balance, 0) +
COALESCE(tb.total_debits, 0) -
COALESCE(tb.total_credits, 0)) AS closing_balance
FROM trial_balance tb
LEFT JOIN opening_balances ob
ON tb.account_id = ob.account_id
ORDER BY tb.ledger;
END;
$function$
-- Function: get_warehouse_details
CREATE OR REPLACE FUNCTION public.get_warehouse_details(p_warehouse_id uuid)
RETURNS TABLE(id uuid, customer_id uuid, name text, address_id uuid, country_name text, country_id uuid, state_name text, state_id uuid, city_name text, city_id uuid, address_line1 text, address_line2 text, zip_code text, gstin text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
wh.id,
wh.customer_id,
wh.name,
wh.address_id,
(SELECT cou.name FROM countries cou WHERE cou.id = a.country_id) AS country_name,
a.country_id,
(SELECT st.name FROM states st WHERE st.id = a.state_id) AS state_name,
a.state_id,
(SELECT ci.name FROM cities ci WHERE ci.id = a.city_id) AS city_name,
a.city_id,
a.address_line1,
a.address_line2,
a.zip_code,
wh.gstin
FROM warehouses wh
JOIN addresses a ON wh.address_id = a.id
WHERE wh.id = p_warehouse_id;
IF NOT FOUND THEN
RETURN; -- Return an empty result set if no warehouse is found
END IF;
END;
$function$
-- Function: close_resolved_cycles
CREATE OR REPLACE FUNCTION public.close_resolved_cycles(p_organization_id uuid, p_company_ids uuid[])
RETURNS void
LANGUAGE plpgsql
AS $function$
BEGIN
UPDATE delinquency_cycles dc
SET
ended_on_utc = now(),
status_id = 2, -- CLOSED
modified_on_utc = now(),
modified_by = get_system_user_id()
WHERE dc.organization_id = p_organization_id
AND dc.ended_on_utc IS NULL
AND dc.is_deleted = false
AND NOT EXISTS (
SELECT 1
FROM invoice_headers ih
WHERE ih.company_id = ANY(p_company_ids)
AND ih.customer_id = dc.customer_id
AND ih.is_deleted = false
AND (ih.total_amount - ih.settled_amount) > 0
);
END;
$function$
-- Function: update_invoice_payment_status
CREATE OR REPLACE FUNCTION public.update_invoice_payment_status(p_company_id uuid, p_invoice_ids uuid[], p_payment_status_id integer, p_modified_by uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
v_invoice_header_id uuid; -- Declare a variable to hold each invoice_header_id
v_invoice_payment_header public.invoice_payment_headers%ROWTYPE; -- Declare ROWTYPE for full row
BEGIN
-- Loop over the provided array of Invoice Header IDs
FOREACH v_invoice_header_id IN ARRAY p_invoice_ids
LOOP
-- Log the current invoice being processed
RAISE NOTICE 'Processing InvoiceHeaderId: %, PaymentStatusId: %, CompanyId: %, ModifiedBy: %',
v_invoice_header_id, p_payment_status_id, p_company_id, p_modified_by;
-- Retrieve the corresponding InvoicePaymentDetail for the given invoice_header_id
SELECT invoice_payment_header_id INTO v_invoice_payment_header.id
FROM public.invoice_payment_details
WHERE invoice_header_id = v_invoice_header_id -- Match invoice header ID in invoice_payment_details
AND is_deleted = false
LIMIT 1;
-- If payment detail exists, proceed to update payment header
IF FOUND THEN
RAISE NOTICE 'Invoice Payment Detail found for InvoiceHeaderId: %', v_invoice_header_id;
-- Retrieve the corresponding InvoicePaymentHeader using the payment header id
SELECT * INTO v_invoice_payment_header
FROM public.invoice_payment_headers
WHERE id = v_invoice_payment_header.id -- Match invoice payment header ID
AND is_deleted = false -- Ensure not deleted
LIMIT 1;
-- If the payment header exists, update its PaymentStatusId
IF FOUND THEN
RAISE NOTICE 'Updating PaymentStatusId for InvoicePaymentHeaderId: %', v_invoice_payment_header.id;
UPDATE public.invoice_payment_headers
SET payment_status_id = p_payment_status_id,
modified_by = p_modified_by,
modified_on_utc = NOW()
WHERE id = v_invoice_payment_header.id;
RAISE NOTICE 'PaymentStatusId updated for InvoicePaymentHeaderId: %', v_invoice_payment_header.id;
ELSE
RAISE NOTICE 'InvoicePaymentHeader not found for InvoiceHeaderId: %', v_invoice_header_id;
END IF;
ELSE
RAISE NOTICE 'InvoicePaymentDetail not found for InvoiceHeaderId: %', v_invoice_header_id;
END IF;
END LOOP;
-- No need for COMMIT, as the transaction will be committed by the caller
RAISE NOTICE 'Updated payment status for % invoices.', array_length(p_invoice_ids, 1);
END;
$function$
-- Function: get_invoices_by_customer_id
CREATE OR REPLACE FUNCTION public.get_invoices_by_customer_id(p_company_id uuid, p_fin_year_id integer, p_user_id uuid, p_customer_id uuid, p_type_id integer DEFAULT NULL::integer, p_start_date timestamp without time zone DEFAULT NULL::timestamp without time zone, p_end_date timestamp without time zone DEFAULT NULL::timestamp without time zone)
RETURNS TABLE(id uuid, invoice_number character varying, type integer, invoice_date date, customer_id uuid, customer_name character varying, due_date date, total_amount numeric, settled_amount numeric, invoice_status_id integer, invoice_status character varying, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, created_by uuid, modified_by uuid, created_by_name character varying, modified_by_name character varying, is_created_by_scheduler boolean, has_next_status_approval boolean, next_status_id integer)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := COALESCE(p_start_date::date, MAKE_DATE(p_fin_year_id, 4, 1));
v_end_date date := COALESCE(p_end_date::date, MAKE_DATE(p_fin_year_id + 1, 3, 31));
DRAFT_STATUS CONSTANT integer := 1;
BEGIN
RETURN QUERY
WITH invoice_data AS (
-- Posted/normal invoices
SELECT
ih.id,
CAST(ih.invoice_number AS varchar) AS invoice_number,
ih.type,
ih.invoice_date::date AS invoice_date,
ih.customer_id,
c.name AS customer_name,
ih.due_date::date AS due_date,
ih.total_amount,
ih.settled_amount,
ih.invoice_status_id,
s.name AS invoice_status,
ih.currency_id,
ih.created_on_utc,
ih.modified_on_utc,
ih.created_by,
ih.modified_by,
CAST(CONCAT(cu.first_name, ' ', cu.last_name) AS varchar) AS created_by_name,
CAST(CONCAT(mu.first_name, ' ', mu.last_name) AS varchar) AS modified_by_name,
ih.is_created_by_scheduler,
ih.credit_account_id
FROM public.invoice_headers ih
LEFT JOIN public.customers c ON c.id = ih.customer_id
LEFT JOIN public.invoice_statuses s ON s.id = ih.invoice_status_id
LEFT JOIN public.users cu ON cu.id = ih.created_by
LEFT JOIN public.users mu ON mu.id = ih.modified_by
WHERE ih.company_id = p_company_id
AND ih.customer_id = p_customer_id
AND ih.is_deleted = false
AND (c.is_deleted = false OR c.is_deleted IS NULL)
AND ih.invoice_date BETWEEN v_start_date AND v_end_date
AND (p_type_id IS NULL OR p_type_id = 0 OR ih.type = p_type_id)
UNION ALL
-- Draft invoices
SELECT
dih.id,
CAST(dih.invoice_number AS varchar) AS invoice_number,
dih.type,
dih.invoice_date::date AS invoice_date,
dih.customer_id,
c.name AS customer_name,
dih.due_date::date AS due_date,
dih.total_amount,
dih.settled_amount,
dih.invoice_status_id,
s.name AS invoice_status,
dih.currency_id,
dih.created_on_utc,
dih.modified_on_utc,
dih.created_by,
dih.modified_by,
CAST(CONCAT(cu.first_name, ' ', cu.last_name) AS varchar) AS created_by_name,
CAST(CONCAT(mu.first_name, ' ', mu.last_name) AS varchar) AS modified_by_name,
dih.is_created_by_scheduler,
dih.credit_account_id
FROM public.draft_invoice_headers dih
LEFT JOIN public.customers c ON c.id = dih.customer_id
LEFT JOIN public.invoice_statuses s ON s.id = dih.invoice_status_id
LEFT JOIN public.users cu ON cu.id = dih.created_by
LEFT JOIN public.users mu ON mu.id = dih.modified_by
WHERE dih.company_id = p_company_id
AND dih.customer_id = p_customer_id
AND dih.is_deleted = false
AND (c.is_deleted = false OR c.is_deleted IS NULL)
AND dih.invoice_date BETWEEN v_start_date AND v_end_date
AND (p_type_id IS NULL OR p_type_id = 0 OR dih.type = p_type_id)
)
SELECT
id.id,
id.invoice_number,
id.type,
id.invoice_date,
id.customer_id,
id.customer_name,
id.due_date,
id.total_amount,
id.settled_amount,
id.invoice_status_id,
id.invoice_status,
id.currency_id,
id.created_on_utc,
id.modified_on_utc,
id.created_by,
id.modified_by,
id.created_by_name,
id.modified_by_name,
id.is_created_by_scheduler,
/* Same approval logic as get_all_invoices_from_span */
CASE
WHEN id.invoice_status_id = DRAFT_STATUS THEN true
WHEN EXISTS (
SELECT 1
FROM public.invoice_approval_users_account a
WHERE a.account_id = id.credit_account_id
AND a.status_id = id.invoice_status_id
AND a.user_id = p_user_id
AND a.is_deleted = false
) THEN true
WHEN EXISTS (
SELECT 1
FROM public.invoice_approval_user_company c
WHERE c.company_id = p_company_id
AND c.status_id = id.invoice_status_id
AND c.user_id = p_user_id
AND c.is_deleted = false
) THEN true
ELSE false
END AS has_next_status_approval,
/* Same next-status resolution as get_all_invoices_from_span */
COALESCE(
(SELECT iw.next_status
FROM public.invoice_workflow iw
WHERE iw.company_id = p_company_id
AND iw.status = id.invoice_status_id
AND iw.is_deleted = false
LIMIT 1),
0
) AS next_status_id
FROM invoice_data id;
END;
$function$
-- Function: list_scheduled_invoice_ids
CREATE OR REPLACE FUNCTION public.list_scheduled_invoice_ids(p_schedule_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP)
RETURNS TABLE(invoice_header_id uuid, draft_invoice_header_id uuid, company_id uuid, organization_id uuid)
LANGUAGE sql
AS $function$
WITH base AS (
SELECT
s.id AS schedule_id,
s.invoice_header_id AS invoice_header_id,
s.draft_invoice_header_id AS draft_invoice_header_id,
s.company_id AS company_id,
c.organization_id AS organization_id,
d.frequency_cycle,
d.day_of_week,
d.day_of_month,
d.month_of_year,
s.starts_from,
s.end_date
FROM public.recurring_sales_schedules s
JOIN public.recurrence_schedule_details d
ON d.schedule_id = s.id
AND d.is_deleted = false
JOIN public.companies c
ON c.id = s.company_id
WHERE s.is_deleted = false
AND s.schedule_status = 'active'
AND s.starts_from <= (p_schedule_date::date)
AND (s.end_date IS NULL OR s.end_date >= (p_schedule_date::date))
),
aligned AS (
SELECT
b.schedule_id,
b.invoice_header_id,
b.draft_invoice_header_id,
b.company_id,
b.organization_id
FROM base b
WHERE CASE lower(b.frequency_cycle)
WHEN 'daily' THEN TRUE
WHEN 'weekly' THEN b.day_of_week IS NOT NULL
AND b.day_of_week = EXTRACT(ISODOW FROM p_schedule_date)::int
WHEN 'monthly' THEN b.day_of_month IS NOT NULL
AND b.day_of_month = EXTRACT(DAY FROM p_schedule_date)::int
WHEN 'yearly' THEN b.month_of_year IS NOT NULL AND b.day_of_month IS NOT NULL
AND b.month_of_year = EXTRACT(MONTH FROM p_schedule_date)::int
AND b.day_of_month = EXTRACT(DAY FROM p_schedule_date)::int
ELSE FALSE
END
)
SELECT DISTINCT
a.invoice_header_id,
a.draft_invoice_header_id,
a.company_id,
a.organization_id
FROM aligned a
WHERE NOT EXISTS (
SELECT 1
FROM public.schedule_execution_logs l
WHERE l.schedule_id = a.schedule_id
AND l.execution_date = (p_schedule_date::date)
AND l.is_deleted = false
);
$function$
-- Function: get_invoice_by_id
CREATE OR REPLACE FUNCTION public.get_invoice_by_id(p_invoice_header_id uuid)
RETURNS TABLE(id uuid, invoice_number text, credit_account_id uuid, invoice_voucher text, invoice_date timestamp with time zone, payment_term integer, customer_id uuid, customer_name character varying, customer_email text, customer_mobile_number text, customer_phone_number text, customer_gstin text, customer_short_name text, customer_pan text, customer_tan text, due_date timestamp with time zone, total_amount numeric, taxable_amount numeric, fees numeric, cgst_amount numeric, sgst_amount numeric, igst_amount numeric, settled_amount numeric, currency_id integer, invoice_status_id integer, invoice_status text, discount numeric, note text, round_off numeric, source_warehouse_id uuid, destination_warehouse_id uuid, destination_customer_id uuid, destination_warehouse_name text, destination_address_id uuid, destination_country_name text, destination_country_id uuid, destination_state_name text, destination_state_id uuid, destination_city_name text, destination_city_id uuid, destination_address_line1 text, destination_address_line2 text, destination_zip_code text, destination_gstin text, invoice_lines jsonb, type integer, current_approval_level integer, is_created_by_scheduler boolean, created_on_utc timestamp with time zone, modified_on_utc timestamp with time zone, created_by uuid, modified_by uuid)
LANGUAGE plpgsql
AS $function$
DECLARE
v_invoice_status_id integer;
BEGIN
-- Get the invoice status using the get_invoice_status function
v_invoice_status_id := public.get_invoice_status(p_invoice_header_id);
IF v_invoice_status_id >= 3 THEN
RETURN QUERY
SELECT
ih.id,
ih.invoice_number,
ih.credit_account_id,
ih.invoice_voucher,
ih.invoice_date::timestamp with time zone,
ih.payment_term,
ih.customer_id,
c.name AS customer_name,
c.email AS customer_email,
c.mobile_number AS customer_mobile_number,
c.phone_number AS customer_phone_number,
c.gstin AS customer_gstin,
c.short_name AS customer_short_name,
c.pan AS customer_pan,
c.tan AS customer_tan,
ih.due_date::timestamp with time zone,
ih.total_amount,
ih.taxable_amount,
ih.fees,
ih.cgst_amount,
ih.sgst_amount,
ih.igst_amount,
ih.settled_amount,
ih.currency_id,
ih.invoice_status_id,
invst.name :: text,
ih.discount,
ih.note,
ih.round_off,
ih.source_warehouse_id,
wd.id AS destination_warehouse_id,
wd.customer_id AS destination_customer_id,
wd.name AS destination_warehouse_name,
wd.address_id AS destination_address_id,
wd.country_name AS destination_country_name,
wd.country_id AS destination_country_id,
wd.state_name AS destination_state_name,
wd.state_id AS destination_state_id,
wd.city_name AS destination_city_name,
wd.city_id AS destination_city_id,
wd.address_line1 AS destination_address_line1,
wd.address_line2 AS destination_address_line2,
wd.zip_code AS destination_zip_code,
wd.gstin AS destination_gstin,
jsonb_agg(jsonb_build_object(
'invoice_line_id', il.id,
'product_id', il.product_id,
'price', il.price,
'quantity', il.quantity,
'fees', il.fees,
'discount', il.discount,
'taxable_amount', il.taxable_amount,
'sgst_amount', il.sgst_amount,
'cgst_amount', il.cgst_amount,
'igst_amount', il.igst_amount,
'total_amount', il.total_amount,
'serial_number', il.serial_number
) ORDER BY il.serial_number) AS invoice_lines,
ih.type,
ih.current_approval_level,
ih.is_created_by_scheduler,
ih.created_on_utc::timestamp with time zone,
ih.modified_on_utc::timestamp with time zone,
ih.created_by,
ih.modified_by
FROM invoice_headers ih
LEFT JOIN public.get_customer_details(ih.customer_id) c ON TRUE
LEFT JOIN public.get_warehouse_details(ih.destination_warehouse_id) wd ON TRUE
LEFT JOIN public.get_invoice_lines(ih.id, ih.invoice_status_id) il ON TRUE
JOIN public.invoice_statuses invst ON ih.invoice_status_id = invst.id
WHERE ih.id = p_invoice_header_id
GROUP BY
ih.id, ih.invoice_number, ih.credit_account_id, ih.invoice_voucher, ih.invoice_date,
ih.payment_term, ih.customer_id, c.name, c.email, c.mobile_number,
c.phone_number, c.gstin, c.short_name, c.pan, c.tan, ih.due_date,
ih.total_amount, ih.taxable_amount, ih.fees, ih.cgst_amount, ih.sgst_amount,
ih.igst_amount, ih.settled_amount, ih.currency_id, ih.invoice_status_id, invst.name, ih.discount, ih.note,
ih.round_off, ih.source_warehouse_id, wd.id, wd.customer_id, wd.name,
wd.address_id, wd.country_name, wd.country_id, wd.state_name, wd.state_id,
wd.city_name, wd.city_id, wd.address_line1, wd.address_line2, wd.zip_code,
wd.gstin,ih.type, ih.current_approval_level, ih.is_created_by_scheduler, ih.created_on_utc, ih.modified_on_utc, ih.created_by, ih.modified_by;
--order by il.serial_number;
ELSE
RETURN QUERY
SELECT
dih.id,
dih.invoice_number,
dih.credit_account_id,
dih.invoice_voucher,
dih.invoice_date::timestamp with time zone,
dih.payment_term,
dih.customer_id,
c.name AS customer_name,
c.email AS customer_email,
c.mobile_number AS customer_mobile_number,
c.phone_number AS customer_phone_number,
c.gstin AS customer_gstin,
c.short_name AS customer_short_name,
c.pan AS customer_pan,
c.tan AS customer_tan,
dih.due_date::timestamp with time zone,
dih.total_amount,
dih.taxable_amount,
dih.fees,
dih.cgst_amount,
dih.sgst_amount,
dih.igst_amount,
dih.settled_amount,
dih.currency_id,
dih.invoice_status_id,
dinvst.name :: text,
dih.discount,
dih.note,
dih.round_off,
dih.source_warehouse_id,
wd.id AS destination_warehouse_id,
wd.customer_id AS destination_customer_id,
wd.name AS destination_warehouse_name,
wd.address_id AS destination_address_id,
wd.country_name AS destination_country_name,
wd.country_id AS destination_country_id,
wd.state_name AS destination_state_name,
wd.state_id AS destination_state_id,
wd.city_name AS destination_city_name,
wd.city_id AS destination_city_id,
wd.address_line1 AS destination_address_line1,
wd.address_line2 AS destination_address_line2,
wd.zip_code AS destination_zip_code,
wd.gstin AS destination_gstin,
jsonb_agg(jsonb_build_object(
'invoice_line_id', il.id,
'product_id', il.product_id,
'price', il.price,
'quantity', il.quantity,
'fees', il.fees,
'discount', il.discount,
'taxable_amount', il.taxable_amount,
'sgst_amount', il.sgst_amount,
'cgst_amount', il.cgst_amount,
'igst_amount', il.igst_amount,
'total_amount', il.total_amount,
'serial_number', il.serial_number
) ORDER BY il.serial_number) AS invoice_lines,
dih.type,
dih.current_approval_level,
dih.is_created_by_scheduler,
dih.created_on_utc::timestamp with time zone,
dih.modified_on_utc::timestamp with time zone,
dih.created_by,
dih.modified_by
FROM draft_invoice_headers dih
LEFT JOIN public.get_customer_details(dih.customer_id) c ON TRUE
LEFT JOIN public.get_warehouse_details(dih.destination_warehouse_id) wd ON TRUE
LEFT JOIN public.get_invoice_lines(dih.id, dih.invoice_status_id) il ON TRUE
JOIN public.invoice_statuses dinvst ON dih.invoice_status_id = dinvst.id
WHERE dih.id = p_invoice_header_id
GROUP BY
dih.id, dih.invoice_number, dih.credit_account_id, dih.invoice_voucher, dih.invoice_date,
dih.payment_term, dih.customer_id, c.name, c.email, c.mobile_number,
c.phone_number, c.gstin, c.short_name, c.pan, c.tan, dih.due_date,
dih.total_amount, dih.taxable_amount, dih.fees, dih.cgst_amount, dih.sgst_amount,
dih.igst_amount, dih.settled_amount, dih.currency_id, dih.invoice_status_id, dinvst.name, dih.discount, dih.note,
dih.round_off, dih.source_warehouse_id, wd.id, wd.customer_id, wd.name,
wd.address_id, wd.country_name, wd.country_id, wd.state_name, wd.state_id,
wd.city_name, wd.city_id, wd.address_line1, wd.address_line2, wd.zip_code,
wd.gstin,dih.type, dih.current_approval_level, dih.is_created_by_scheduler, dih.created_on_utc, dih.modified_on_utc, dih.created_by, dih.modified_by;
--order by il.serial_number;
END IF;
END;
$function$
-- Function: fetch_all_invoices
CREATE OR REPLACE FUNCTION public.fetch_all_invoices(company_id uuid, fin_year_id integer)
RETURNS TABLE(invoice_id uuid, invoice_number text, credit_account_id uuid, invoice_voucher text, customer_id uuid, customer_name text, due_date date, invoice_date date, payment_term text, total_amount numeric, setteled_amount numeric, currency_id uuid, invoice_status text, invoice_status_id uuid, discount numeric, note text, created_on timestamp without time zone, modified_on timestamp without time zone, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name text, invoice_lines jsonb, invoice_type text, is_created_by_scheduler boolean)
LANGUAGE plpgsql
AS $function$
DECLARE
financial_year_start DATE := MAKE_DATE(fin_year_id, 4, 1);
financial_year_end DATE := MAKE_DATE(fin_year_id + 1, 3, 31);
BEGIN
RETURN QUERY
SELECT
ih.id AS invoice_id,
ih.invoice_number,
ih.credit_account_id,
ih.invoice_voucher,
ih.customer_id,
(SELECT c.name FROM customers c WHERE c.id = ih.customer_id) AS customer_name,
ih.due_date,
ih.invoice_date,
ih.payment_term,
ih.total_amount,
ih.settled_amount,
ih.currency_id,
(SELECT ist.name FROM invoice_statuses ist WHERE ist.id = ih.invoice_status_id) AS invoice_status,
ih.invoice_status_id,
ih.discount,
ih.note,
ih.created_on_utc AS created_on,
ih.modified_on_utc AS modified_on,
ih.created_by,
ih.modified_by,
ih.source_warehouse_id,
ih.destination_warehouse_id,
(SELECT w.name FROM warehouses w WHERE w.id = ih.destination_warehouse_id) AS destination_warehouse_name,
(SELECT JSONB_AGG(
JSONB_BUILD_OBJECT(
'id', id,
'product_id', product_id,
'price', price,
'quantity', quantity,
'fees', fees,
'discount', discount,
'taxable_amount', taxable_amount,
'sgst_amount', sgst_amount,
'cgst_amount', cgst_amount,
'igst_amount', igst_amount,
'total_amount', total_amount,
'serial_number', serial_number
)
)
FROM invoice_details
WHERE invoice_header_id = ih.id) AS invoice_lines,
ih.type AS invoice_type,
ih.is_created_by_scheduler
FROM invoice_headers ih
WHERE ih.company_id = company_id
AND ih.invoice_date BETWEEN financial_year_start AND financial_year_end
UNION ALL
SELECT
dih.id AS invoice_id,
dih.invoice_number,
dih.credit_account_id,
dih.invoice_voucher,
dih.customer_id,
(SELECT c.name FROM customers c WHERE c.id = dih.customer_id) AS customer_name,
dih.due_date,
dih.invoice_date,
dih.payment_term,
dih.total_amount,
dih.settled_amount,
dih.currency_id,
(SELECT ist.name FROM invoice_statuses ist WHERE ist.id = dih.invoice_status_id) AS invoice_status,
dih.invoice_status_id,
dih.discount,
dih.note,
dih.created_on_utc AS created_on,
dih.modified_on_utc AS modified_on,
dih.created_by,
dih.modified_by,
dih.source_warehouse_id,
dih.destination_warehouse_id,
(SELECT w.name FROM warehouses w WHERE w.id = dih.destination_warehouse_id) AS destination_warehouse_name,
(SELECT JSONB_AGG(
JSONB_BUILD_OBJECT(
'id', id,
'product_id', product_id,
'price', price,
'quantity', quantity,
'fees', fees,
'discount', discount,
'taxable_amount', taxable_amount,
'sgst_amount', sgst_amount,
'cgst_amount', cgst_amount,
'igst_amount', igst_amount,
'total_amount', total_amount,
'serial_number', serial_number
)
)
FROM draft_invoice_details
WHERE invoice_header_id = dih.id) AS invoice_lines,
dih.type AS invoice_type,
dih.is_created_by_scheduler
FROM draft_invoice_headers dih
WHERE dih.company_id = company_id
AND dih.invoice_date BETWEEN financial_year_start AND financial_year_end;
END;
$function$
-- Function: apply_penalty_for_due_invoices
CREATE OR REPLACE FUNCTION public.apply_penalty_for_due_invoices(p_product_id uuid, p_company_id uuid, p_penalty_receivable_account_id uuid, p_revenue_account_id uuid, p_created_by uuid, p_batch_size integer DEFAULT 10, p_penalty_date date DEFAULT CURRENT_DATE)
RETURNS TABLE(invoice_header_id uuid, credit_account uuid, debit_account uuid, company_id uuid, customer_id uuid, invoice_number text, penalty_amount numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
cursor_invoice CURSOR FOR
SELECT
ih.id AS invoice_header_id,
ih.total_amount,
ih.settled_amount,
ih.customer_id,
ih.invoice_number,
ih.due_date,
pc.percentage as percentage,
pf.name AS frequency
FROM
invoice_headers ih
JOIN
penalty_configs pc ON ih.penalty_config_id = pc.id
JOIN
penalty_frequencies pf ON pc.penalty_frequency_id = pf.id
LEFT JOIN
penalty_processing_logs pl ON ih.id = pl.invoice_id AND pl.processing_date = p_penalty_date
WHERE
ih.company_id = p_company_id
AND public.check_penalty_date(ih.due_date::date, p_penalty_date) -- Apply penalty one day after the due date
AND ih.invoice_status_id IN (3, 4)-- Approved or Partially Paid
AND (pl.invoice_id IS NULL OR pl.status = 'failed'); -- Only process unprocessed or failed ones
invoice RECORD;
penalty_amount NUMERIC;
frequency_factor NUMERIC;
base_amount NUMERIC;
batch_count INT := 0;
invoice_header_id uuid;
invoice_details_id uuid;
BEGIN
-- Create a temporary table to store results
CREATE TEMP TABLE temp_penalty_results (
invoice_header_id UUID,
credit_account UUID,
debit_account UUID,
company_id UUID,
customer_id UUID,
invoice_number TEXT,
penalty_amount NUMERIC
) ON COMMIT DROP;
OPEN cursor_invoice;
LOOP
-- Begin a new transaction batch
BEGIN
-- Fetch the first record in the current batch
FETCH cursor_invoice INTO invoice;
EXIT WHEN NOT FOUND; -- Exit if no more records to process
-- Process up to p_batch_size records within this transaction
FOR i IN 1 .. p_batch_size LOOP
-- Exit the inner loop if there are no more records
EXIT WHEN NOT FOUND;
BEGIN
-- Track the penalty application start
INSERT INTO penalty_processing_logs (invoice_id, processing_date, status)
VALUES (invoice.invoice_header_id, p_penalty_date, 'pending')
ON CONFLICT (invoice_id, processing_date) DO UPDATE SET status = 'pending';
-- Calculate penalty amount
base_amount := CASE
WHEN invoice.settled_amount IS NOT NULL THEN (invoice.total_amount - invoice.settled_amount)
ELSE invoice.total_amount
END;
RAISE NOTICE 'base_amount : %', base_amount;
frequency_factor := CASE invoice.frequency
WHEN 'Yearly' THEN (invoice.percentage / 100) / 12
WHEN 'Monthly' THEN (invoice.percentage / 100)
ELSE 0
END;
RAISE NOTICE 'frequency_factor : %', frequency_factor;
penalty_amount := ROUND(base_amount * frequency_factor);
RAISE NOTICE 'penalty_amount : %', penalty_amount;
SELECT gen_random_uuid() INTO invoice_details_id;
SELECT gen_random_uuid() INTO invoice_header_id;
-- Insert penalty into invoice_details
--insert into invoice_headers
CALL public.create_invoice_header(
invoice_header_id,
p_company_id,
invoice.customer_id,
p_penalty_receivable_account_id,
p_revenue_account_id,
p_penalty_date,
invoice.due_date::date,
0,
penalty_amount,
0,
0,
0,
penalty_amount,
0,
0,
0,
1,
'penalty charges',
3,
p_created_by,
'inv',
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000000',
null,
null,
5
);
--insert into invoice_details
CALL public.create_invoice_detail(
invoice_header_id,
penalty_amount,
1,
0,
0,
0,
0,
0,
penalty_amount,
penalty_amount,
1,
p_product_id
);
RAISE NOTICE 'successful inserted in invoiceDetails';
-- Mark the penalty application as completed in the log
UPDATE penalty_processing_logs
SET status = 'completed'
WHERE invoice_id = invoice.invoice_header_id;
RAISE NOTICE 'successful updated in penalty_processing_logs';
-- Insert data into the temporary table for returning at the end
INSERT INTO temp_penalty_results (
invoice_header_id, credit_account, debit_account, company_id, customer_id, invoice_number, penalty_amount
) VALUES (
invoice.invoice_header_id, p_revenue_account_id, p_penalty_receivable_account_id, p_company_id,
invoice.customer_id, invoice.invoice_number, penalty_amount
);
-- Fetch the next invoice for processing within the same batch
FETCH cursor_invoice INTO invoice;
EXCEPTION
WHEN OTHERS THEN
-- Mark the failed invoices as 'failed' in the log
UPDATE penalty_processing_logs
SET status = 'failed'
WHERE invoice_id = invoice.invoice_header_id
AND processing_date = p_penalty_date;
-- Log the error
RAISE NOTICE 'Error in processing invoice %: %', invoice.invoice_header_id, SQLERRM;
END;
END LOOP;
-- Increment batch count for reference
batch_count := batch_count + 1;
END;
END LOOP;
CLOSE cursor_invoice;
-- Return all data from the temporary table at the end
RETURN QUERY SELECT * FROM temp_penalty_results;
END;
$function$
-- Function: get_all_invoices
CREATE OR REPLACE FUNCTION public.get_all_invoices(company_id uuid, fin_year_id integer)
RETURNS TABLE(invoice_id uuid, invoice_number character varying, credit_account_id uuid, invoice_voucher character varying, customer_id uuid, customer_name character varying, due_date date, invoice_date date, payment_term character varying, total_amount numeric, settled_amount numeric, currency_id integer, invoice_status character varying, invoice_status_id integer, discount numeric, note character varying, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name character varying, invoice_lines jsonb, type character varying, is_created_by_scheduler boolean)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
ih.id AS invoice_id,
ih.invoice_number::character varying, -- Cast to character varying
ih.credit_account_id,
ih.invoice_voucher::character varying, -- Cast to character varying
ih.customer_id,
(SELECT c.name::character varying FROM customers c WHERE c.id = ih.customer_id) AS customer_name,
ih.due_date::date, -- Cast to date
ih.invoice_date::date, -- Cast to date
ih.payment_term::character varying, -- Cast to character varying
ih.total_amount,
ih.settled_amount,
ih.currency_id,
(SELECT s.name::character varying FROM invoice_statuses s WHERE s.id = ih.invoice_status_id) AS invoice_status,
ih.invoice_status_id, -- Already integer
ih.discount,
ih.note::character varying, -- Cast to character varying
ih.created_on_utc,
ih.modified_on_utc,
ih.created_by,
ih.modified_by,
ih.source_warehouse_id,
ih.destination_warehouse_id,
(SELECT w.name::character varying FROM warehouses w WHERE w.id = ih.destination_warehouse_id) AS destination_warehouse_name,
(
SELECT jsonb_agg(
jsonb_build_object(
'id', d.id,
'product_id', d.product_id,
'price', d.price,
'quantity', d.quantity,
'fees', d.fees,
'discount', d.discount,
'taxable_amount', d.taxable_amount,
'sgst_amount', d.sgst_amount,
'cgst_amount', d.cgst_amount,
'igst_amount', d.igst_amount,
'total_amount', d.total_amount,
'serial_number', d.serial_number
)
)
FROM invoice_details d WHERE d.invoice_header_id = ih.id
) AS invoice_lines,
ih.type::character varying, -- Cast to character varying
ih.is_created_by_scheduler
FROM invoice_headers ih
WHERE ih.company_id = get_all_invoices.company_id
AND ih.invoice_date BETWEEN MAKE_DATE(get_all_invoices.fin_year_id, 4, 1) AND MAKE_DATE(get_all_invoices.fin_year_id + 1, 3, 31)
UNION ALL
SELECT
dih.id AS invoice_id,
dih.invoice_number::character varying, -- Cast to character varying
dih.credit_account_id,
dih.invoice_voucher::character varying, -- Cast to character varying
dih.customer_id,
(SELECT c.name::character varying FROM customers c WHERE c.id = dih.customer_id) AS customer_name,
dih.due_date::date, -- Cast to date
dih.invoice_date::date, -- Cast to date
dih.payment_term::character varying, -- Cast to character varying
dih.total_amount,
dih.settled_amount,
dih.currency_id,
(SELECT s.name::character varying FROM invoice_statuses s WHERE s.id = dih.invoice_status_id) AS invoice_status,
dih.invoice_status_id, -- Already integer
dih.discount,
dih.note::character varying, -- Cast to character varying
dih.created_on_utc,
dih.modified_on_utc,
dih.created_by,
dih.modified_by,
dih.source_warehouse_id,
dih.destination_warehouse_id,
(SELECT w.name::character varying FROM warehouses w WHERE w.id = dih.destination_warehouse_id) AS destination_warehouse_name,
(
SELECT jsonb_agg(
jsonb_build_object(
'id', d.id,
'product_id', d.product_id,
'price', d.price,
'quantity', d.quantity,
'fees', d.fees,
'discount', d.discount,
'taxable_amount', d.taxable_amount,
'sgst_amount', d.sgst_amount,
'cgst_amount', d.cgst_amount,
'igst_amount', d.igst_amount,
'total_amount', d.total_amount,
'serial_number', d.serial_number
)
)
FROM draft_invoice_details d WHERE d.invoice_header_id = dih.id
) AS invoice_lines,
dih.type::character varying, -- Cast to character varying
dih.is_created_by_scheduler
FROM draft_invoice_headers dih
WHERE dih.company_id = get_all_invoices.company_id
AND dih.invoice_date BETWEEN MAKE_DATE(get_all_invoices.fin_year_id, 4, 1) AND MAKE_DATE(get_all_invoices.fin_year_id + 1, 3, 31);
END;
$function$
-- Function: get_grouped_invoice_header
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_header(p_company_id uuid, p_fin_year_id integer)
RETURNS TABLE(id uuid, group_invoice_number text, grouped_invoice_template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text, group_invoice_batch_id text, paid_count integer, total_paid_amount numeric, original_total_amount numeric, remaining_amount numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
BEGIN
RETURN QUERY
SELECT
gih.id,
gih.group_invoice_number,
gih.group_invoice_template_id,
gih.execution_date,
gih.error_message,
gih.success_count,
gih.total_count,
templates.invoice_description,
gih.group_invoice_batch_id,
/* Paid invoice count (fully settled invoices only) */
CAST(
COALESCE(
SUM(
CASE
WHEN ih.settled_amount >= ih.total_amount THEN 1
ELSE 0
END
),
0
) AS integer
) AS paid_count,
/* ✅ FIX: Cap payment per invoice */
COALESCE(
SUM(
LEAST(ih.settled_amount, ih.total_amount)
),
0
) AS total_paid_amount,
/* Original invoice total */
COALESCE(
SUM(ih.total_amount),
0
) AS original_total_amount,
/* ✅ FIX: Never negative remaining */
COALESCE(
SUM(
GREATEST(ih.total_amount - ih.settled_amount, 0)
),
0
) AS remaining_amount
FROM public.group_invoice_headers gih
INNER JOIN public.group_invoice_templates templates
ON templates.id = gih.group_invoice_template_id
LEFT JOIN public.group_invoice_details gid
ON gid.group_invoice_header_id = gih.id
AND gid.is_deleted = false
AND gid.company_id = p_company_id
LEFT JOIN public.invoice_headers ih
ON ih.id = gid.invoice_header_id
AND ih.is_deleted = false
AND ih.company_id = p_company_id
WHERE gih.company_id = p_company_id
AND gih.execution_date::date BETWEEN v_start_date AND v_end_date
GROUP BY
gih.id,
gih.group_invoice_number,
gih.group_invoice_template_id,
gih.execution_date,
gih.error_message,
gih.success_count,
gih.total_count,
templates.invoice_description,
gih.group_invoice_batch_id
ORDER BY gih.execution_date DESC;
END;
$function$
-- Function: get_grouped_invoice_details
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_details(p_group_invoice_header_id uuid)
RETURNS TABLE(invoice_header_id uuid, invoice_number text, invoice_date timestamp without time zone, payment_term numeric, due_date timestamp without time zone, total_amount numeric, settled_amount numeric, taxable_amount numeric, discount numeric, sgst_amount numeric, cgst_amount numeric, igst_amount numeric, round_off numeric, note text, customer_id uuid, customer_name text, invoice_status_id integer, invoice_status text, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, credit_account_id uuid, invoice_voucher text, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name text, type integer, is_created_by_scheduler boolean)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
ih.id AS invoice_header_id,
ih.invoice_number::text AS invoice_number, -- Explicit cast
ih.invoice_date,
ih.payment_term::numeric AS payment_term, -- Cast to numeric
ih.due_date,
ih.total_amount,
ih.settled_amount,
ih.taxable_amount,
ih.discount,
ih.sgst_amount,
ih.cgst_amount,
ih.igst_amount,
ih.round_off,
ih.note::text AS note, -- Explicit cast
ih.customer_id,
COALESCE(c.name, 'Unknown')::text AS customer_name, -- Explicit cast
ih.invoice_status_id,
COALESCE(s.name, 'Unknown')::text AS invoice_status, -- Explicit cast
ih.currency_id,
ih.created_on_utc,
ih.modified_on_utc,
ih.credit_account_id,
ih.invoice_voucher::text AS invoice_voucher, -- Explicit cast
ih.created_by,
ih.modified_by,
ih.source_warehouse_id,
ih.destination_warehouse_id,
COALESCE(w.name, 'Unknown')::text AS destination_warehouse_name, -- Explicit cast
ih.type,
ih.is_created_by_scheduler
FROM
public.group_invoice_details aid
INNER JOIN
invoice_headers ih ON aid.invoice_header_id = ih.id
LEFT JOIN
customers c ON ih.customer_id = c.id
LEFT JOIN
invoice_statuses s ON ih.invoice_status_id = s.id
LEFT JOIN
warehouses w ON ih.destination_warehouse_id = w.id
WHERE
aid.group_invoice_header_id = p_group_invoice_header_id
AND NOT aid.is_deleted
AND NOT ih.is_deleted;
END;
$function$
-- Function: get_unit_dues_by_cust_id
CREATE OR REPLACE FUNCTION public.get_unit_dues_by_cust_id(p_company_id uuid, p_customer_id uuid)
RETURNS TABLE(id uuid, name text, total_due numeric, total_paid numeric, original_amount numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.id::uuid,
c.name::text,
COALESCE(SUM(
CASE
WHEN i.due_date < now()
AND i.is_deleted = false
AND (i.total_amount - i.settled_amount) > 0
THEN (i.total_amount - i.settled_amount)
ELSE 0
END
), 0)::numeric AS total_due,
COALESCE(SUM(
CASE
WHEN i.due_date < now()
AND i.is_deleted = false
THEN i.settled_amount
ELSE 0
END
), 0)::numeric AS total_paid,
COALESCE(SUM(i.total_amount), 0)::numeric AS original_amount
FROM public.customers c
LEFT JOIN public.invoice_headers i
ON i.customer_id = c.id AND i.company_id = p_company_id
WHERE c.is_deleted = false
AND c.company_id = p_company_id
AND c.id = p_customer_id
GROUP BY c.id, c.name;
END;
$function$
-- Function: get_all_customers_details
CREATE OR REPLACE FUNCTION public.get_all_customers_details(p_company_id uuid)
RETURNS TABLE(id uuid, name character varying, contact_name text, gst_in text, mobile_number text, email text, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, billing_address jsonb, contact_count integer)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.id,
c.name,
con.first_name || ' ' || con.last_name AS contact_name,
c.gstin AS gst_in,
con.mobile_number,
con.email,
c.created_by,
u_created.first_name || ' ' || u_created.last_name AS created_name,
c.modified_by,
u_modified.first_name || ' ' || u_modified.last_name AS modified_name,
c.created_on_utc,
c.modified_on_utc,
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,
'StateId', ba.state_id,
'CityId', ba.city_id
)
ELSE NULL
END AS billing_address,
(SELECT COUNT(*)::integer -- Cast to integer
FROM public.customer_contacts cc_count
WHERE cc_count.customer_id = c.id) AS contact_count -- Contact count subquery
FROM
public.customers c
LEFT JOIN
public.customer_contacts cc ON c.id = cc.customer_id
LEFT JOIN
public.contacts con ON cc.contact_id = con.id And con.is_primary = true
LEFT JOIN
public.users u_created ON c.created_by = u_created.id
LEFT JOIN
public.users u_modified ON c.modified_by = u_modified.id
LEFT JOIN
public.addresses ba ON c.billing_address_id = ba.id
WHERE
c.company_id = p_company_id
AND c.is_deleted = false;
--AND con.is_primary = true;
END;
$function$
-- Function: get_customer_by_id
CREATE OR REPLACE FUNCTION public.get_customer_by_id(p_customer_id uuid)
RETURNS TABLE(id uuid, name text, gstin text, short_name text, pan text, tan text, proprietor_name text, outstanding_limit numeric, is_non_work boolean, interest_percentage numeric, billing_address_id uuid, billing_address jsonb, shipping_address_id uuid, shipping_address jsonb, bank_accounts jsonb, contacts jsonb, contact jsonb)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.id,
c.name::text, -- Cast to text
c.gstin::text, -- Cast to text
c.short_name::text, -- Cast to text
c.pan::text, -- Cast to text
c.tan::text, -- Cast to text
c.proprietor_name::text, -- Cast to text
c.outstanding_limit,
c.is_non_work,
COALESCE(c.interest_percentage, 0) AS interest_percentage, -- Ensure default 0 if NULL
c.billing_address_id,
-- Billing address as JSONB
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,
'StateId', ba.state_id,
'StateName', COALESCE(s.name, ''), -- Ensure empty string if NULL
'CityId', ba.city_id,
'CityName', COALESCE(ct.name, '') -- Ensure empty string if NULL
)
ELSE NULL
END AS billing_address,
c.shipping_address_id,
-- Shipping address as JSONB
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,
'StateId', sa.state_id,
'StateName', COALESCE(s2.name, ''), -- Ensure empty string if NULL
'CityId', sa.city_id,
'CityName', COALESCE(ct2.name, '') -- Ensure empty string if NULL
)
ELSE NULL
END AS shipping_address,
-- Bank accounts as JSONB array
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'Id', ba.id,
'BankId', ba.bank_id,
'BranchName', ba.branch_name,
'BankName', b.name,
'AccountNumber', ba.account_number,
'IFSC', ba.ifsc
)
)
FROM customer_bank_accounts cba
JOIN bank_accounts ba ON cba.bank_account_id = ba.id
JOIN banks b ON ba.bank_id = b.id
WHERE cba.customer_id = p_customer_id AND ba.is_deleted = false
),
'[]'::jsonb -- Ensure empty array if no results
) AS bank_accounts,
-- Customer contacts as JSONB array
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'Id', con.id,
'Salutation', con.salutation,
'FirstName', con.first_name,
'LastName', con.last_name,
'Email', con.email,
'PhoneNumber', con.phone_number,
'MobileNumber', con.mobile_number,
'IsPrimary', con.is_primary
)
)
FROM customer_contacts cc
JOIN contacts con ON cc.contact_id = con.id
WHERE cc.customer_id = p_customer_id AND con.is_deleted = false
),
'[]'::jsonb -- Ensure empty array if no results
) AS contacts,
-- Primary contact as JSONB
COALESCE(
(
SELECT jsonb_build_object(
'Id', con.id,
'Salutation', con.salutation,
'FirstName', con.first_name,
'LastName', con.last_name,
'Email', con.email,
'PhoneNumber', con.phone_number,
'MobileNumber', con.mobile_number
)
FROM customer_contacts cc
JOIN contacts con ON cc.contact_id = con.id
WHERE cc.customer_id = p_customer_id AND con.is_primary = TRUE AND con.is_deleted = false
LIMIT 1
),
NULL
) AS contact
FROM customers c
LEFT JOIN addresses ba ON c.billing_address_id = ba.id
LEFT JOIN addresses sa ON c.shipping_address_id = sa.id
-- Joining states and cities for billing address
LEFT JOIN states s ON ba.state_id = s.id
LEFT JOIN cities ct ON ba.city_id = ct.id
-- Joining states and cities for shipping address
LEFT JOIN states s2 ON sa.state_id = s2.id
LEFT JOIN cities ct2 ON sa.city_id = ct2.id
WHERE c.id = p_customer_id AND c.is_deleted = false;
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_invoice_company_approvers
CREATE OR REPLACE FUNCTION public.get_invoice_company_approvers(p_company_id uuid)
RETURNS TABLE(id integer, level1_user_id uuid, level2_user_id uuid, payment_approver_user_id uuid)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
1 AS id,
(ARRAY_AGG(ia.user_id) FILTER (WHERE ia.status_id = 8))[1] AS level1_user_id,
(ARRAY_AGG(ia.user_id) FILTER (WHERE ia.status_id = 9))[1] AS level2_user_id,
(ARRAY_AGG(ia.user_id) FILTER (WHERE ia.status_id = 5))[1] AS payment_approver_user_id
FROM invoice_approval_user_company ia
WHERE ia.company_id = p_company_id
AND ia.is_deleted = false;
END;
$function$
-- Function: create_user
CREATE OR REPLACE FUNCTION public.create_user(p_user_id uuid, p_email text, p_phone_number text, p_first_name text, p_last_name text, p_created_by uuid, p_company_id 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,
email,
phone_number,
first_name,
last_name,
password_hash,
created_on_utc,
created_by,
company_id
) VALUES (
p_user_id, -- Generate a new UUID for the user 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
NOW(), -- Current timestamp
p_created_by, -- Created by
p_company_id -- Link to the provided company
);
-- Return the new user ID
RETURN p_user_id;
END;
$function$
-- Function: get_account_total_amount_expense
CREATE OR REPLACE FUNCTION public.get_account_total_amount_expense(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;
BEGIN
-- Step 1: 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 2: 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 3: Calculate the total amount for the specific account within the financial year
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
WHERE th.company_id = p_company_id
AND je.account_id = p_account_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE;
-- Step 4: Return the total amount
RETURN v_total_amount;
END;
$function$
-- Function: get_all_customers_details_byorg
CREATE OR REPLACE FUNCTION public.get_all_customers_details_byorg(p_organization_id uuid)
RETURNS TABLE(id uuid, name character varying, contact_name text, gst_in text, mobile_number text, email text, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, billing_address jsonb, contact_count integer)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.id,
c.name,
con.first_name || ' ' || con.last_name AS contact_name,
c.gstin AS gst_in,
con.mobile_number,
con.email,
c.created_by,
u_created.first_name || ' ' || u_created.last_name AS created_name,
c.modified_by,
u_modified.first_name || ' ' || u_modified.last_name AS modified_name,
c.created_on_utc,
c.modified_on_utc,
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,
'StateId', ba.state_id,
'CityId', ba.city_id
)
ELSE NULL
END AS billing_address,
(SELECT COUNT(*)::integer -- Cast to integer
FROM public.customer_contacts cc_count
WHERE cc_count.customer_id = c.id) AS contact_count -- Contact count subquery
FROM
public.customers c
JOIN
public.customer_contacts cc ON c.id = cc.customer_id
JOIN
public.contacts con ON cc.contact_id = con.id
LEFT JOIN
public.users u_created ON c.created_by = u_created.id
LEFT JOIN
public.users u_modified ON c.modified_by = u_modified.id
LEFT JOIN
public.addresses ba ON c.billing_address_id = ba.id
JOIN
public.companies comp ON c.company_id = comp.id -- Join companies table
WHERE
comp.organization_id = p_organization_id -- Filter by organization_id
AND c.is_deleted = false
AND con.is_primary = true;
END;
$function$
-- Function: get_all_invoice_account_approvers
CREATE OR REPLACE FUNCTION public.get_all_invoice_account_approvers(p_company_id uuid)
RETURNS TABLE(id integer, account_id uuid, status_id integer, user_id uuid, approval_level integer, required_approval_levels integer)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
ia.id,
ia.account_id,
ia.status_id, -- Use ia.status_id here (the user's approval status)
ia.user_id,
ia.approval_level,
iaal.approval_level_required AS required_approval_levels
FROM invoice_approval_users_account ia
JOIN invoice_account_approval_levels iaal
ON ia.company_id = iaal.company_id
AND ia.account_id = iaal.account_id
WHERE ia.company_id = p_company_id
AND ia.is_deleted = FALSE
AND iaal.is_deleted = FALSE
AND iaal.status_id = 2 -- Filter for status_id = 2
ORDER BY ia.account_id, ia.status_id;
END;
$function$
-- Function: upsert_customer_default_account
CREATE OR REPLACE FUNCTION public.upsert_customer_default_account(p_updates jsonb)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
update_record JSONB;
v_customer_id UUID;
v_account_id UUID;
v_company_id UUID;
v_user_id UUID;
BEGIN
FOR update_record IN SELECT * FROM jsonb_array_elements(p_updates)
LOOP
v_customer_id := update_record->>'CustomerId';
v_account_id := update_record->>'AccountId';
v_company_id := update_record->>'CompanyId';
v_user_id := update_record->>'UserId';
INSERT INTO customer_default_accounts (
customer_id, account_id, company_id, created_by, created_on_utc, is_deleted
)
VALUES (
v_customer_id, v_account_id, v_company_id, v_user_id, now(), false
)
ON CONFLICT (customer_id, company_id) DO UPDATE
SET
account_id = EXCLUDED.account_id,
modified_by = EXCLUDED.created_by,
modified_on_utc = now(),
is_deleted = false;
END LOOP;
END;
$function$
-- Function: get_invoice_approval_log
CREATE OR REPLACE FUNCTION public.get_invoice_approval_log(p_invoice_id uuid)
RETURNS TABLE(status integer, status_name text, next_status integer, next_status_name text, previous_status integer, previous_status_name text, is_initial boolean, approver_user_id uuid, approver_user_name text, approved_by uuid, approved_by_name text, approved_on timestamp without time zone, invoice_id uuid, sales_account uuid, created_by_name text, approval_level integer)
LANGUAGE plpgsql
AS $function$
BEGIN
IF EXISTS (SELECT 1 FROM public.invoice_headers WHERE id = p_invoice_id) THEN
RETURN QUERY
SELECT
iw.status,
cs.name::TEXT AS status_name,
iw.next_status,
ns.name::TEXT AS next_status_name,
iw.previous_status,
ps.name::TEXT AS previous_status_name,
iw.is_initial,
COALESCE(iaua.user_id, iauc.user_id) AS approver_user_id,
CONCAT(COALESCE(uaa.first_name, uca.first_name), ' ', COALESCE(uaa.last_name, uca.last_name))::TEXT AS approver_user_name,
ial.approved_by,
CONCAT(approved_by_user.first_name, ' ', approved_by_user.last_name)::TEXT AS approved_by_name,
ial.approved_on,
ih.id AS invoice_id,
ih.credit_account_id AS sales_account,
CONCAT(created_by_user.first_name, ' ', created_by_user.last_name)::TEXT AS created_by_name,
iw.approval_level
FROM invoice_workflow iw
JOIN invoice_headers ih ON ih.id = p_invoice_id
LEFT JOIN invoice_statuses cs ON cs.id = iw.status
LEFT JOIN invoice_statuses ns ON ns.id = iw.next_status
LEFT JOIN invoice_statuses ps ON ps.id = iw.previous_status
LEFT JOIN invoice_approval_user_company iauc
ON iauc.company_id = ih.company_id
AND iauc.status_id = iw.status
AND iauc.approval_level = iw.approval_level
LEFT JOIN invoice_approval_users_account iaua
ON iaua.company_id = ih.company_id
AND iaua.account_id = ih.credit_account_id
AND iaua.status_id = iw.status
AND iaua.approval_level = iw.approval_level
LEFT JOIN users uca ON uca.id = iauc.user_id
LEFT JOIN users uaa ON uaa.id = iaua.user_id
LEFT JOIN users created_by_user ON created_by_user.id = ih.created_by
LEFT JOIN LATERAL (
SELECT * FROM invoice_approval_logs log
WHERE log.invoice_id = ih.id
AND log.status_id = iw.status
AND log.approval_level = iw.approval_level
ORDER BY log.approved_on DESC
LIMIT 1
) ial ON true
LEFT JOIN users approved_by_user ON approved_by_user.id = ial.approved_by
WHERE iw.company_id = ih.company_id
AND iw.is_deleted = false
AND iw.is_enabled = true
ORDER BY iw.approval_level;
ELSE
RETURN QUERY
SELECT
iw.status,
cs.name::TEXT AS status_name,
iw.next_status,
ns.name::TEXT AS next_status_name,
iw.previous_status,
ps.name::TEXT AS previous_status_name,
iw.is_initial,
COALESCE(iaua.user_id, iauc.user_id) AS approver_user_id,
CONCAT(COALESCE(uaa.first_name, uca.first_name), ' ', COALESCE(uaa.last_name, uca.last_name))::TEXT AS approver_user_name,
NULL::UUID AS approved_by,
NULL::TEXT AS approved_by_name,
NULL::TIMESTAMP AS approved_on,
dih.id AS invoice_id,
dih.credit_account_id AS sales_account,
CONCAT(created_by_user.first_name, ' ', created_by_user.last_name)::TEXT AS created_by_name,
iw.approval_level
FROM invoice_workflow iw
JOIN draft_invoice_headers dih ON dih.id = p_invoice_id
LEFT JOIN invoice_statuses cs ON cs.id = iw.status
LEFT JOIN invoice_statuses ns ON ns.id = iw.next_status
LEFT JOIN invoice_statuses ps ON ps.id = iw.previous_status
LEFT JOIN invoice_approval_user_company iauc
ON iauc.company_id = dih.company_id
AND iauc.status_id = iw.status
AND iauc.approval_level = iw.approval_level
LEFT JOIN invoice_approval_users_account iaua
ON iaua.company_id = dih.company_id
AND iaua.account_id = dih.credit_account_id
AND iaua.status_id = iw.status
AND iaua.approval_level = iw.approval_level
LEFT JOIN users uca ON uca.id = iauc.user_id
LEFT JOIN users uaa ON uaa.id = iaua.user_id
LEFT JOIN users created_by_user ON created_by_user.id = dih.created_by
WHERE iw.company_id = dih.company_id
AND iw.is_deleted = false
AND iw.is_enabled = true
ORDER BY iw.approval_level;
END IF;
END;
$function$
-- Function: get_invoice_lines
CREATE OR REPLACE FUNCTION public.get_invoice_lines(p_invoice_header_id uuid, p_invoice_status_id integer)
RETURNS TABLE(id uuid, product_id uuid, price numeric, quantity numeric, fees numeric, discount numeric, taxable_amount numeric, sgst_amount numeric, cgst_amount numeric, igst_amount numeric, total_amount numeric, serial_number integer)
LANGUAGE plpgsql
AS $function$
BEGIN
IF p_invoice_status_id >= 3 THEN
RETURN QUERY
SELECT
ids.id,
ids.product_id,
ids.price,
ids.quantity,
ids.fees,
ids.discount,
ids.taxable_amount,
ids.sgst_amount,
ids.cgst_amount,
ids.igst_amount,
ids.total_amount,
ids.serial_number
FROM invoice_details ids
WHERE ids.invoice_header_id = p_invoice_header_id and ids.is_deleted = false
ORDER BY ids.serial_number;
ELSE
RETURN QUERY
SELECT
did.id,
did.product_id,
did.price,
did.quantity,
did.fees,
did.discount,
did.taxable_amount,
did.sgst_amount,
did.cgst_amount,
did.igst_amount,
did.total_amount,
did.serial_number
FROM draft_invoice_details did
WHERE did.invoice_header_id = p_invoice_header_id and did.is_deleted = false
ORDER BY did.serial_number;
END IF;
END;
$function$
-- Function: get_new_draft_invoice_number
CREATE OR REPLACE FUNCTION public.get_new_draft_invoice_number(p_company_id uuid, p_date date)
RETURNS character varying
LANGUAGE plpgsql
AS $function$
DECLARE
v_finance_year integer;
new_invoice_id integer;
p_prefix varchar;
invoice_number varchar;
BEGIN
-- Determine the start year of the financial year based on p_date
IF EXTRACT(MONTH FROM p_date) >= 4 THEN
v_finance_year := EXTRACT(YEAR FROM p_date);
ELSE
v_finance_year := EXTRACT(YEAR FROM p_date) - 1;
END IF;
-- Attempt to update the last_invoice_id for the given company and financial year
WITH x AS (
UPDATE public.draft_invoice_header_ids
SET last_invoice_id = last_invoice_id + 1
WHERE company_id = p_company_id AND fin_year = v_finance_year
RETURNING last_invoice_id, invoice_prefix
),
insert_x AS (
-- If no rows were updated, insert a new row with the default prefix 'DIN' and default invoice_length
INSERT INTO public.draft_invoice_header_ids (company_id, fin_year, invoice_prefix, last_invoice_id, invoice_length)
SELECT p_company_id, v_finance_year, 'DIN', 1, 8 -- Default invoice_length set to 8
WHERE NOT EXISTS (SELECT 1 FROM x)
RETURNING last_invoice_id, invoice_prefix
)
-- Use COALESCE to return the correct last_invoice_id and invoice_prefix
SELECT COALESCE((SELECT last_invoice_id FROM x LIMIT 1), (SELECT last_invoice_id FROM insert_x LIMIT 1)),
COALESCE((SELECT invoice_prefix FROM x LIMIT 1), (SELECT invoice_prefix FROM insert_x LIMIT 1))
INTO new_invoice_id, p_prefix;
-- Concatenate the prefix and new_invoice_id to form the invoice number
invoice_number := p_prefix || LPAD(new_invoice_id::text, 4, '0');
-- Return the generated invoice number
RETURN invoice_number;
END;
$function$
-- Function: bulk_delete_customers
CREATE OR REPLACE FUNCTION public.bulk_delete_customers(p_customer_ids uuid[], p_modified_by uuid, p_deleted_on_utc timestamp without time zone)
RETURNS TABLE(id integer, customer_id uuid, status text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_id UUID;
v_blocked_count INT;
row_index INT := 1;
BEGIN
FOREACH v_id IN ARRAY p_customer_ids LOOP
-- 1. Skip if customer already deleted
IF NOT EXISTS (
SELECT 1 FROM public.customers c WHERE c.id = v_id AND c.is_deleted = false
) THEN
id := row_index;
bulk_delete_customers.customer_id := v_id;
bulk_delete_customers.status := 'Skipped - Customer Not Found or Already Deleted';
row_index := row_index + 1;
RETURN NEXT;
CONTINUE;
END IF;
-- 2. Check if any financial invoices exist
SELECT COUNT(*) INTO v_blocked_count
FROM (
SELECT 1 FROM public.invoice_headers ih
WHERE ih.customer_id = v_id AND ih.is_deleted = false AND ih.invoice_status_id IN (3, 4, 5)
UNION ALL
SELECT 1 FROM public.draft_invoice_headers dih
WHERE dih.customer_id = v_id AND dih.is_deleted = false AND dih.invoice_status_id IN (3, 4, 5)
) AS financial_invoices;
IF v_blocked_count > 0 THEN
id := row_index;
bulk_delete_customers.customer_id := v_id;
bulk_delete_customers.status := 'Blocked - Financial Invoices Exist (Approved/Partially Paid/Paid)';
row_index := row_index + 1;
RETURN NEXT;
CONTINUE;
END IF;
-- 3. Soft delete eligible draft invoice details
UPDATE public.draft_invoice_details did
SET is_deleted = true,
deleted_on_utc = p_deleted_on_utc
WHERE did.invoice_header_id IN (
SELECT dih.id FROM public.draft_invoice_headers dih
WHERE dih.customer_id = v_id AND dih.is_deleted = false AND dih.invoice_status_id NOT IN (3, 4, 5)
);
-- 4. Soft delete eligible draft invoice headers
UPDATE public.draft_invoice_headers dih
SET is_deleted = true,
modified_by = p_modified_by,
deleted_on_utc = p_deleted_on_utc
WHERE dih.customer_id = v_id AND dih.is_deleted = false AND dih.invoice_status_id NOT IN (3, 4, 5);
-- 5. Soft delete eligible final invoice details
UPDATE public.invoice_details id
SET is_deleted = true,
deleted_on_utc = p_deleted_on_utc
WHERE id.invoice_header_id IN (
SELECT ih.id FROM public.invoice_headers ih
WHERE ih.customer_id = v_id AND ih.is_deleted = false AND ih.invoice_status_id NOT IN (3, 4, 5)
);
-- 6. Soft delete eligible final invoice headers
UPDATE public.invoice_headers ih
SET is_deleted = true,
modified_by = p_modified_by,
deleted_on_utc = p_deleted_on_utc
WHERE ih.customer_id = v_id AND ih.is_deleted = false AND ih.invoice_status_id NOT IN (3, 4, 5);
-- 7. Soft delete customer
UPDATE public.customers c
SET is_deleted = true,
modified_by = p_modified_by,
deleted_on_utc = p_deleted_on_utc
WHERE c.id = v_id;
-- 8. Return success
id := row_index;
bulk_delete_customers.customer_id := v_id;
bulk_delete_customers.status := 'Deleted';
row_index := row_index + 1;
RETURN NEXT;
END LOOP;
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;
BEGIN
-- Step 1: 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 2: 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 3: Calculate the total amount for the specific account within the financial year
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
WHERE th.company_id = p_company_id
AND je.account_id = p_account_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE;
-- Step 4: Return the total amount
RETURN v_total_amount;
END;
$function$
-- Function: get_all_customer_notes
CREATE OR REPLACE FUNCTION public.get_all_customer_notes(p_company_id uuid)
RETURNS TABLE(id uuid, company_id uuid, customer_id uuid, customer_name text, note_date date, invoice_id text, customer_note_status_id integer, customer_note_status text, is_debit_note boolean, total_amount numeric, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
cnh.id,
cnh.company_id,
cnh.customer_id,
c.name::TEXT AS customer_name, -- Cast to TEXT
cnh.note_date::DATE, -- Cast to DATE
cnh.invoice_id::TEXT,
cnh.customer_note_status_id,
cn_status.name AS customer_note_status,
cnh.is_debit_note,
cnh.total_amount,
cnh.created_on_utc,
cnh.modified_on_utc,
cnh.created_by,
(SELECT CONCAT(u.first_name, ' ', u.last_name) FROM users u WHERE u.id = cnh.created_by) AS created_by_name,
cnh.modified_by,
(SELECT CONCAT(u.first_name, ' ', u.last_name) FROM users u WHERE u.id = cnh.modified_by) AS modified_by_name
FROM customer_note_headers cnh
JOIN customers c ON cnh.customer_id = c.id
JOIN customer_note_statuses cn_status ON cnh.customer_note_status_id = cn_status.id
WHERE cnh.company_id = p_company_id;
END;
$function$
-- Function: get_all_invoice_headers
CREATE OR REPLACE FUNCTION public.get_all_invoice_headers(p_company_id uuid)
RETURNS TABLE(id uuid, invoice_number text, invoice_voucher text, customer_id uuid, customer_name text, due_date timestamp with time zone, invoice_date timestamp with time zone, payment_term integer, total_amount numeric, settled_amount numeric, discount numeric, note text, currency_id integer, invoice_status_id integer, invoice_status text, type integer, is_created_by_scheduler boolean, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
-- Query InvoiceHeaders
SELECT header.id, header.invoice_number, header.invoice_voucher, header.customer_id,
customer.name::text AS customer_name,
header.due_date::timestamptz, header.invoice_date::timestamptz, header.payment_term,
header.total_amount, header.settled_amount, header.discount, header.note,
header.currency_id, header.invoice_status_id, status.name::text AS invoice_status, header.type, header.is_created_by_scheduler,
header.created_by,
COALESCE(u_created.first_name || ' ' || u_created.last_name, 'N/A') AS created_by_name, -- full name or 'N/A'
header.modified_by,
COALESCE(u_modified.first_name || ' ' || u_modified.last_name, 'N/A') AS modified_by_name -- full name or 'N/A'
FROM invoice_headers header
JOIN customers customer ON customer.id = header.customer_id
JOIN invoice_statuses status ON status.id = header.invoice_status_id
LEFT JOIN users u_created ON u_created.id = header.created_by -- join to get the created_by user's full name
LEFT JOIN users u_modified ON u_modified.id = header.modified_by -- join to get the modified_by user's full name
WHERE header.company_id = p_company_id
AND header.is_deleted = false
UNION ALL
-- Query DraftInvoiceHeaders
SELECT draft.id, draft.invoice_number, draft.invoice_voucher, draft.customer_id,
customer.name::text AS customer_name,
draft.due_date::timestamptz, draft.invoice_date::timestamptz, draft.payment_term,
draft.total_amount, draft.settled_amount, draft.discount, draft.note,
draft.currency_id, draft.invoice_status_id, status.name::text AS invoice_status, draft.type, draft.is_created_by_scheduler,
draft.created_by,
COALESCE(u_created.first_name || ' ' || u_created.last_name, 'N/A') AS created_by_name, -- full name or 'N/A'
draft.modified_by,
COALESCE(u_modified.first_name || ' ' || u_modified.last_name, 'N/A') AS modified_by_name -- full name or 'N/A'
FROM draft_invoice_headers draft
JOIN customers customer ON customer.id = draft.customer_id
JOIN invoice_statuses status ON status.id = draft.invoice_status_id
LEFT JOIN users u_created ON u_created.id = draft.created_by -- join to get the created_by user's full name
LEFT JOIN users u_modified ON u_modified.id = draft.modified_by -- join to get the modified_by user's full name
WHERE draft.company_id = p_company_id
AND draft.is_deleted = false;
END;
$function$
-- Function: get_group_invoice_stats
CREATE OR REPLACE FUNCTION public.get_group_invoice_stats(p_group_invoice_header_id uuid)
RETURNS TABLE(paid_count integer, paid_amount numeric, unpaid_count integer, unpaid_amount numeric, overdue_count integer, overdue_amount numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
-- Validate if the p_group_invoice_header_id belongs to a valid group invoice header
IF NOT EXISTS (
SELECT 1
FROM group_invoice_headers gih
WHERE gih.id = p_group_invoice_header_id
AND gih.is_deleted = false
) THEN
RAISE EXCEPTION 'Invalid group_invoice_header_id: It is either deleted or does not exist.';
END IF;
-- Calculate grouped invoice statistics
RETURN QUERY
SELECT
-- Paid
COALESCE(COUNT(CASE WHEN ih.invoice_status_id = 5 THEN 1 ELSE NULL END), 0)::INTEGER AS paid_count,
COALESCE(SUM(CASE WHEN ih.invoice_status_id = 5 THEN ih.total_amount ELSE 0 END), 0) AS paid_amount,
-- Unpaid (Draft, Pending Approval, Approved, or Partially Paid)
COALESCE(COUNT(CASE WHEN ih.invoice_status_id IN (1, 2, 3, 4) THEN 1 ELSE NULL END), 0)::INTEGER AS unpaid_count,
COALESCE(SUM(CASE WHEN ih.invoice_status_id IN (1, 2, 3, 4) THEN ih.total_amount ELSE 0 END), 0) AS unpaid_amount,
-- Overdue
COALESCE(COUNT(CASE WHEN ih.invoice_status_id NOT IN (5) AND ih.due_date < now() THEN 1 ELSE NULL END), 0)::INTEGER AS overdue_count,
COALESCE(SUM(CASE WHEN ih.invoice_status_id NOT IN (5) AND ih.due_date < now() THEN ih.total_amount ELSE 0 END), 0) AS overdue_amount
FROM group_invoice_details gid
INNER JOIN invoice_headers ih
ON ih.id = gid.invoice_header_id
WHERE gid.group_invoice_header_id = p_group_invoice_header_id
AND gid.is_deleted = false;
END;
$function$
-- Function: get_invoice_workflow_config
CREATE OR REPLACE FUNCTION public.get_invoice_workflow_config(p_company_id uuid)
RETURNS TABLE(id integer, name text, is_enabled boolean, next_statuses integer[], previous_statuses integer[])
LANGUAGE sql
AS $function$
SELECT
s.id,
s.name,
-- Dynamically determine if at least one transition from this status is enabled
COALESCE((
SELECT bool_or(w.is_enabled)
FROM invoice_workflow w
WHERE w.company_id = p_company_id
AND w.status = s.id
AND w.is_deleted = false
), false) AS is_enabled,
-- Forward links (next_statuses)
COALESCE(ARRAY(
SELECT w.next_status
FROM invoice_workflow w
WHERE w.company_id = p_company_id
AND w.status = s.id
AND w.is_deleted = false
AND w.is_enabled = true
ORDER BY w.next_status
), ARRAY[]::INT[]) AS next_statuses,
-- Backward links (previous_statuses)
COALESCE(ARRAY(
SELECT DISTINCT w.previous_status
FROM invoice_workflow w
WHERE w.company_id = p_company_id
AND w.status = s.id
AND w.is_deleted = false
AND w.is_enabled = true
AND w.previous_status IS NOT NULL
ORDER BY w.previous_status
), ARRAY[]::INT[]) AS previous_statuses
FROM invoice_statuses s
WHERE s.is_deleted = false;
$function$
-- Function: get_new_invoice_number
CREATE OR REPLACE FUNCTION public.get_new_invoice_number(p_company_id uuid, p_date date)
RETURNS character varying
LANGUAGE plpgsql
AS $function$
DECLARE
v_finance_year integer;
new_invoice_id integer;
p_prefix varchar;
invoice_number varchar;
BEGIN
-- Determine the start year of the financial year based on p_date
IF EXTRACT(MONTH FROM p_date) >= 4 THEN
v_finance_year := EXTRACT(YEAR FROM p_date);
ELSE
v_finance_year := EXTRACT(YEAR FROM p_date) - 1;
END IF;
-- Attempt to update the last_invoice_id for the given company and financial year
WITH x AS (
UPDATE public.invoice_header_ids
SET last_invoice_id = last_invoice_id + 1
WHERE company_id = p_company_id AND fin_year = v_finance_year
RETURNING last_invoice_id, invoice_prefix
),
insert_x AS (
-- If no rows were updated, insert a new row with the default prefix 'INV' and default invoice_length
INSERT INTO public.invoice_header_ids (company_id, fin_year, invoice_prefix, last_invoice_id, invoice_length)
SELECT p_company_id, v_finance_year, 'INV', 1, 8 -- Default invoice_length set to 8
WHERE NOT EXISTS (SELECT 1 FROM x)
RETURNING last_invoice_id, invoice_prefix
)
-- Use COALESCE to return the correct last_invoice_id and invoice_prefix
SELECT COALESCE((SELECT last_invoice_id FROM x LIMIT 1), (SELECT last_invoice_id FROM insert_x LIMIT 1)),
COALESCE((SELECT invoice_prefix FROM x LIMIT 1), (SELECT invoice_prefix FROM insert_x LIMIT 1))
INTO new_invoice_id, p_prefix;
-- Concatenate the prefix and new_invoice_id to form the invoice number
invoice_number := p_prefix || LPAD(new_invoice_id::text, 4, '0');
-- Return the generated invoice number
RETURN invoice_number;
END;
$function$
-- Function: update_invoice_next_status_main
CREATE OR REPLACE FUNCTION public.update_invoice_next_status_main(p_company_id uuid, p_invoice_ids uuid[], p_modified_by uuid)
RETURNS TABLE(invoice_id uuid, status_name text, error_msg text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_error_message text;
v_invoice_ids_draft uuid[];
v_invoice_ids_pending uuid[];
v_invoice_ids_other uuid[];
v_next_temp_id int;
BEGIN
RAISE NOTICE 'START update_invoice_next_status_main: company_id=%, modified_by=%', p_company_id, p_modified_by;
RAISE NOTICE 'Incoming Invoice IDs: %', p_invoice_ids;
-- Clean up temp_invoice_next_status for these invoice IDs to avoid duplicate entries
DELETE FROM temp_invoice_next_status tinv
WHERE tinv.invoice_id = ANY(p_invoice_ids);
-- Classify invoices by current status
SELECT array_agg(id) INTO v_invoice_ids_draft
FROM public.draft_invoice_headers dih
WHERE dih.id = ANY(p_invoice_ids) AND dih.invoice_status_id = 1;
SELECT array_agg(id) INTO v_invoice_ids_pending
FROM public.draft_invoice_headers dih
WHERE dih.id = ANY(p_invoice_ids) AND dih.invoice_status_id = 2;
SELECT array_agg(id) INTO v_invoice_ids_other
FROM public.invoice_headers ih
WHERE ih.id = ANY(p_invoice_ids) AND ih.invoice_status_id >= 3;
RAISE NOTICE 'Draft invoices: %', COALESCE(v_invoice_ids_draft, '{}');
RAISE NOTICE 'Pending Approval invoices: %', COALESCE(v_invoice_ids_pending, '{}');
RAISE NOTICE 'Other (Approved or higher) invoices: %', COALESCE(v_invoice_ids_other, '{}');
BEGIN
-- Direct update draft invoices to Pending Approval
IF array_length(v_invoice_ids_draft, 1) > 0 THEN
RAISE NOTICE 'Directly updating Draft invoices to Pending Approval';
UPDATE public.draft_invoice_headers
SET invoice_status_id = 2,
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = ANY(v_invoice_ids_draft);
-- Insert approval logs
INSERT INTO public.invoice_approval_logs(
invoice_id, status_id, approved_by, approved_on, "comment",
created_on_utc, created_by, approval_level
)
SELECT
dh.id, 2, p_modified_by, now(),
'Direct update from Draft to Pending Approval',
now(), p_modified_by, 0
FROM public.draft_invoice_headers dh
WHERE dh.id = ANY(v_invoice_ids_draft);
-- Generate next id for temp_invoice_next_status
SELECT COALESCE(MAX(id), 0) INTO v_next_temp_id FROM temp_invoice_next_status;
-- Insert into temp_invoice_next_status
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
SELECT
row_number() OVER () + v_next_temp_id AS id,
dh.id,
'Pending Approval',
NULL
FROM public.draft_invoice_headers dh
WHERE dh.id = ANY(v_invoice_ids_draft);
END IF;
-- Call update_draft_invoice_next_status if Pending Approval invoices found and p_invoice_status_id = 2
IF array_length(v_invoice_ids_pending, 1) > 0 THEN
RAISE NOTICE 'Calling update_draft_invoice_next_status for Pending Approval invoices';
CALL public.update_draft_invoice_next_status(p_company_id, v_invoice_ids_pending, p_modified_by);
END IF;
-- Call update_invoice_next_status_for_invoice_header for Approved or higher invoices
IF array_length(v_invoice_ids_other, 1) > 0 THEN
RAISE NOTICE 'Calling update_invoice_next_status_for_invoice_header for Approved or higher invoices';
CALL public.update_invoice_next_status_for_invoice_header(p_company_id, v_invoice_ids_other, p_modified_by);
END IF;
EXCEPTION WHEN OTHERS THEN
v_error_message := SQLERRM;
RAISE NOTICE 'Exception in child procedures: %', v_error_message;
END;
-- Return rows from temp_invoice_next_status
RAISE NOTICE 'Preparing to return rows from temp_invoice_next_status';
RETURN QUERY
SELECT tinv.invoice_id, tinv.status AS status_name, tinv.error AS error_msg
FROM temp_invoice_next_status tinv
WHERE tinv.invoice_id = ANY(p_invoice_ids); -- Directly filter by the input invoice IDs
END;
$function$
-- Function: get_invoice_header_by_id
CREATE OR REPLACE FUNCTION public.get_invoice_header_by_id(p_invoice_header_id uuid)
RETURNS TABLE(id uuid, invoice_number text, invoice_voucher text, customer_id uuid, customer_name text, due_date timestamp with time zone, invoice_date timestamp with time zone, payment_term integer, total_amount numeric, settled_amount numeric, discount numeric, note text, currency_id integer, invoice_status_id integer, invoice_status text, type integer, is_created_by_scheduler boolean)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
-- Query for the specific InvoiceHeader
SELECT header.id,
header.invoice_number,
header.invoice_voucher,
customer.id AS customer_id,
customer.name::text AS customer_name, -- Cast to text
header.due_date::timestamptz,
header.invoice_date::timestamptz,
header.payment_term,
header.total_amount,
header.settled_amount,
header.discount,
header.note,
header.currency_id,
status.id AS invoice_status_id,
status.name::text AS invoice_status, -- Cast to text
header.type,
header.is_created_by_scheduler
FROM invoice_headers header
JOIN customers customer ON customer.id = header.customer_id
JOIN invoice_statuses status ON status.id = header.invoice_status_id
WHERE header.id = p_invoice_header_id
AND header.is_deleted = false;
END;
$function$
-- Function: get_invoice_payments_by_customer_id
CREATE OR REPLACE FUNCTION public.get_invoice_payments_by_customer_id(p_company_id uuid, p_customer_id uuid, p_from_date date, p_to_date date)
RETURNS TABLE(company_id uuid, customer_id uuid, customer_name character varying, id uuid, payment_number text, mode_of_payment text, reference text, received_date timestamp without time zone, received_amount numeric, grand_total_amount numeric, advance_amount numeric, tds_amount numeric, description text, debit_account_id uuid, credit_account_id uuid, transaction_id bigint, is_posted boolean, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, is_deleted boolean)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
iph.company_id,
iph.customer_id,
c.name AS customer_name,
iph.id AS id,
iph.payment_number,
iph.mode_of_payment,
iph.reference,
iph.received_date,
iph.received_amount,
iph.grand_total_amount,
iph.advance_amount,
iph.tds_amount,
iph.description,
iph.debit_account_id,
iph.credit_account_id,
iph.transaction_id,
iph.is_posted,
iph.created_on_utc,
iph.modified_on_utc,
iph.is_deleted
FROM
invoice_payment_headers iph
INNER JOIN
customers c ON iph.customer_id = c.id
WHERE
iph.company_id = p_company_id
AND iph.customer_id = p_customer_id
AND iph.is_deleted = FALSE
AND c.is_deleted = FALSE
AND iph.received_date BETWEEN p_from_date AND p_to_date
ORDER BY
iph.received_date DESC;
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, p_from_date date, p_to_date date)
RETURNS TABLE(company_id uuid, customer_id uuid, customer_name character varying, id uuid, invoice_number text, invoice_voucher text, invoice_date timestamp without time zone, due_date timestamp without time zone, total_amount numeric, settled_amount numeric, due_amount numeric, payment_status_id integer, fin_entry_status_id integer, transaction_id bigint, is_posted boolean, current_approval_level integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, is_deleted boolean)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
i.company_id,
i.customer_id,
c.name AS customer_name,
i.id AS id,
i.invoice_number,
i.invoice_voucher,
i.invoice_date,
i.due_date,
i.total_amount,
i.settled_amount,
(i.total_amount - COALESCE(i.settled_amount, 0))::numeric AS due_amount,
i.payment_status_id,
i.fin_entry_status_id,
i.transaction_id,
i.is_posted,
i.current_approval_level,
i.created_on_utc,
i.modified_on_utc,
i.is_deleted
FROM
invoice_headers i
INNER JOIN
customers c ON i.customer_id = c.id
WHERE
i.company_id = p_company_id
AND i.customer_id = p_customer_id
AND i.is_deleted = FALSE
AND c.is_deleted = FALSE
-- Unpaid or partially paid invoices
AND (i.settled_amount IS NULL OR i.settled_amount < i.total_amount)
-- Due date filter
AND i.due_date BETWEEN p_from_date AND p_to_date
ORDER BY
i.due_date ASC;
END;
$function$
-- Function: get_customers_with_invoice_summary
CREATE OR REPLACE FUNCTION public.get_customers_with_invoice_summary(p_company_id uuid, p_fin_year_id integer)
RETURNS TABLE(id uuid, name character varying, contact_name text, gst_in text, mobile_number text, email text, created_by uuid, created_by_name text, modified_by uuid, modified_by_name text, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, billing_address jsonb, contact_count integer, total_invoice_amount numeric, total_settled_amount numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
BEGIN
RETURN QUERY
SELECT
c.id,
c.name,
con.first_name || ' ' || con.last_name AS contact_name,
c.gstin AS gst_in,
con.mobile_number,
con.email,
c.created_by,
u_created.first_name || ' ' || u_created.last_name AS created_by_name,
c.modified_by,
u_modified.first_name || ' ' || u_modified.last_name AS modified_by_name,
c.created_on_utc,
c.modified_on_utc,
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,
'StateId', ba.state_id,
'CityId', ba.city_id
)
ELSE NULL
END AS billing_address,
(SELECT COUNT(*)::integer FROM public.customer_contacts cc_count WHERE cc_count.customer_id = c.id) AS contact_count,
COALESCE(inv_sums.total_invoice_amount, 0) AS total_invoice_amount,
COALESCE(inv_sums.total_settled_amount, 0) AS total_settled_amount
FROM public.customers c
LEFT JOIN public.customer_contacts cc ON c.id = cc.customer_id
LEFT JOIN public.contacts con ON cc.contact_id = con.id AND con.is_primary = true
LEFT JOIN public.users u_created ON c.created_by = u_created.id
LEFT JOIN public.users u_modified ON c.modified_by = u_modified.id
LEFT JOIN public.addresses ba ON c.billing_address_id = ba.id
LEFT JOIN (
SELECT
ih.customer_id,
SUM(ih.total_amount) AS total_invoice_amount,
SUM(ih.settled_amount) AS total_settled_amount
FROM public.invoice_headers ih
WHERE ih.company_id = p_company_id
AND ih.is_deleted = false
AND ih.invoice_date >= v_start_date
AND ih.invoice_date <= v_end_date
GROUP BY ih.customer_id
) inv_sums ON inv_sums.customer_id = c.id
WHERE c.company_id = p_company_id
AND c.is_deleted = false;
END;
$function$
-- Function: get_defaultors
CREATE OR REPLACE FUNCTION public.get_defaultors(p_company_id uuid)
RETURNS TABLE(id uuid, customer_name text, invoice_number text, created_date timestamp without time zone, due_date timestamp without time zone, amount numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
-- Calculate and return the list of defaultors (overdue invoices)
RETURN QUERY
SELECT
gen_random_uuid() AS id, -- Generate a random unique ID
c.name::TEXT, -- Customer Name
i.invoice_number, -- Invoice Number
i.created_on_utc AS created_date, -- Created Date
i.due_date, -- Due Date
i.total_amount AS amount -- Total Invoice Amount
FROM
invoice_headers i
JOIN
customers c ON i.customer_id = c.id
JOIN
company_defaultor_configurations cfg ON i.company_id = cfg.company_id
WHERE
i.company_id = p_company_id
AND i.due_date < CURRENT_DATE - INTERVAL '1 day' * cfg.due_period_days -- Overdue invoices based on company configuration
AND i.settled_amount < i.total_amount -- Only unpaid invoices
AND i.is_deleted = FALSE -- Exclude deleted invoices
AND c.is_deleted = FALSE; -- Exclude deleted customers
END;
$function$
-- Function: get_execution_invoices
CREATE OR REPLACE FUNCTION public.get_execution_invoices(p_execution_id uuid)
RETURNS TABLE(invoice_header_id uuid, invoice_number text, invoice_date timestamp without time zone, payment_term numeric, due_date timestamp without time zone, total_amount numeric, settled_amount numeric, taxable_amount numeric, discount numeric, sgst_amount numeric, cgst_amount numeric, igst_amount numeric, round_off numeric, note text, customer_id uuid, customer_name text, invoice_status_id integer, invoice_status text, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, credit_account_id uuid, invoice_voucher text, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name text, type integer, is_created_by_scheduler boolean)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
ih.id AS invoice_header_id,
ih.invoice_number::text AS invoice_number, -- Explicit cast
ih.invoice_date,
ih.payment_term::numeric AS payment_term, -- Cast to numeric
ih.due_date,
ih.total_amount,
ih.settled_amount,
ih.taxable_amount,
ih.discount,
ih.sgst_amount,
ih.cgst_amount,
ih.igst_amount,
ih.round_off,
ih.note::text AS note, -- Explicit cast
ih.customer_id,
COALESCE(c.name, 'Unknown')::text AS customer_name, -- Explicit cast
ih.invoice_status_id,
COALESCE(s.name, 'Unknown')::text AS invoice_status, -- Explicit cast
ih.currency_id,
ih.created_on_utc,
ih.modified_on_utc,
ih.credit_account_id,
ih.invoice_voucher::text AS invoice_voucher, -- Explicit cast
ih.created_by,
ih.modified_by,
ih.source_warehouse_id,
ih.destination_warehouse_id,
COALESCE(w.name, 'Unknown')::text AS destination_warehouse_name, -- Explicit cast
ih.type,
ih.is_created_by_scheduler
FROM
apartment_invoice_postings aip
INNER JOIN
invoice_headers ih ON aip.invoice_header_id = ih.id
LEFT JOIN
customers c ON ih.customer_id = c.id
LEFT JOIN
invoice_statuses s ON ih.invoice_status_id = s.id
LEFT JOIN
warehouses w ON ih.destination_warehouse_id = w.id
WHERE
aip.execution_id = p_execution_id
AND NOT aip.is_deleted
AND NOT ih.is_deleted;
END;
$function$
-- Function: get_total_defaultors_and_amount
CREATE OR REPLACE FUNCTION public.get_total_defaultors_and_amount(p_company_id uuid)
RETURNS TABLE(result_id uuid, total_defaultors integer, total_amount numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
-- Generate a unique id for this result
result_id := gen_random_uuid();
-- Debugging: Output the company_id being passed
RAISE NOTICE 'Calculating for company_id: %', p_company_id;
-- Calculate total defaultors and total amount for overdue invoices
RETURN QUERY
SELECT
result_id As id, -- Return the generated unique id for each result
COUNT(DISTINCT i.customer_id)::INT AS total_defaultors, -- Cast COUNT to INT
SUM(i.total_amount) AS total_amount -- Sum of total amounts of overdue invoices
FROM
invoice_headers i
JOIN
customers c ON i.customer_id = c.id -- Join with customers to filter by company_id
WHERE
i.company_id = p_company_id -- Use the passed-in parameter (p_company_id)
AND i.due_date < CURRENT_DATE - (SELECT due_period_days FROM company_defaultor_configurations WHERE company_id = p_company_id LIMIT 1) -- Check for overdue invoices based on due_period_days
AND i.settled_amount < i.total_amount -- Only unpaid invoices (settled_amount < total_amount)
AND i.is_deleted = FALSE -- Exclude deleted invoices
AND c.is_deleted = FALSE; -- Exclude deleted customers
END;
$function$
-- Function: get_grouped_invoice_details
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_details(p_group_invoice_header_id uuid, p_fin_year integer)
RETURNS TABLE(invoice_header_id uuid, invoice_number text, invoice_date timestamp without time zone, payment_term numeric, due_date timestamp without time zone, total_amount numeric, settled_amount numeric, taxable_amount numeric, discount numeric, sgst_amount numeric, cgst_amount numeric, igst_amount numeric, round_off numeric, note text, customer_id uuid, customer_name text, invoice_status_id integer, invoice_status text, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, credit_account_id uuid, invoice_voucher text, created_by uuid, modified_by uuid, source_warehouse_id uuid, destination_warehouse_id uuid, destination_warehouse_name text, type integer, is_created_by_scheduler boolean)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := MAKE_DATE(p_fin_year, 4, 1);
v_end_date date := MAKE_DATE(p_fin_year + 1, 3, 31);
BEGIN
RETURN QUERY
SELECT
ih.id AS invoice_header_id,
ih.invoice_number::text AS invoice_number,
ih.invoice_date,
ih.payment_term::numeric AS payment_term,
ih.due_date,
ih.total_amount,
ih.settled_amount,
ih.taxable_amount,
ih.discount,
ih.sgst_amount,
ih.cgst_amount,
ih.igst_amount,
ih.round_off,
ih.note::text AS note,
ih.customer_id,
COALESCE(c.name, 'Unknown')::text AS customer_name,
ih.invoice_status_id,
COALESCE(s.name, 'Unknown')::text AS invoice_status,
ih.currency_id,
ih.created_on_utc,
ih.modified_on_utc,
ih.credit_account_id,
ih.invoice_voucher::text AS invoice_voucher,
ih.created_by,
ih.modified_by,
ih.source_warehouse_id,
ih.destination_warehouse_id,
COALESCE(w.name, 'Unknown')::text AS destination_warehouse_name,
ih.type,
ih.is_created_by_scheduler
FROM
public.group_invoice_details aid
INNER JOIN
invoice_headers ih ON aid.invoice_header_id = ih.id
LEFT JOIN
customers c ON ih.customer_id = c.id
LEFT JOIN
invoice_statuses s ON ih.invoice_status_id = s.id
LEFT JOIN
warehouses w ON ih.destination_warehouse_id = w.id
WHERE
aid.group_invoice_header_id = p_group_invoice_header_id
AND NOT aid.is_deleted
AND NOT ih.is_deleted
AND (p_fin_year IS NULL OR (ih.invoice_date >= v_start_date AND ih.invoice_date <= v_end_date));
END;
$function$
-- Function: get_group_invoice_totals
CREATE OR REPLACE FUNCTION public.get_group_invoice_totals(p_company_id uuid, p_grouped_invoice_id uuid)
RETURNS TABLE(id uuid, grouped_invoice_name text, total_amount numeric, paid_amount numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
gih.id AS id,
gih.group_invoice_number AS grouped_invoice_name,
SUM(ih.total_amount) AS total_amount,
SUM(
CASE
WHEN ih.invoice_status_id = 3 THEN ih.settled_amount
ELSE 0
END
) AS paid_amount
FROM
public.group_invoice_headers gih
JOIN
public.group_invoice_details gid ON gih.id = gid.group_invoice_header_id
JOIN
public.invoice_headers ih ON gid.invoice_header_id = ih.id
WHERE
gih.id = p_grouped_invoice_id
AND gih.company_id = p_company_id
AND gih.is_deleted = false
GROUP BY
gih.id,
gih.group_invoice_number;
END;
$function$
-- Function: get_invoice_analytics
CREATE OR REPLACE FUNCTION public.get_invoice_analytics(p_company_id uuid, p_finance_year_id integer, p_group_invoice_header_id uuid DEFAULT NULL::uuid)
RETURNS TABLE(id integer, status text, count integer, total_amount numeric, month_start_date date)
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
BEGIN
v_financial_year_start := TO_DATE(p_finance_year_id || '-04-01', 'YYYY-MM-DD');
v_financial_year_end := TO_DATE((p_finance_year_id + 1) || '-03-31', 'YYYY-MM-DD');
-- Case 1: Group Invoice Specific
IF p_group_invoice_header_id IS NOT NULL THEN
IF NOT EXISTS (
SELECT 1
FROM group_invoice_headers gih
WHERE gih.id = p_group_invoice_header_id
AND gih.is_deleted = false
) THEN
RETURN;
END IF;
RETURN QUERY
SELECT row_number() OVER ()::int AS id,
'Paid' AS status,
COUNT(*)::int,
COALESCE(SUM(ih.settled_amount),0),
DATE_TRUNC('month', ih.invoice_date)::date
FROM group_invoice_details gid
JOIN invoice_headers ih ON ih.id = gid.invoice_header_id
WHERE gid.group_invoice_header_id = p_group_invoice_header_id
AND gid.is_deleted = false
AND ih.invoice_status_id IN (4,5)
AND ih.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
GROUP BY DATE_TRUNC('month', ih.invoice_date)
UNION ALL
SELECT row_number() OVER ()::int,
'Pending',
COUNT(*)::int,
COALESCE(SUM(ih.total_amount - ih.settled_amount),0),
DATE_TRUNC('month', ih.invoice_date)::date
FROM group_invoice_details gid
JOIN invoice_headers ih ON ih.id = gid.invoice_header_id
WHERE gid.group_invoice_header_id = p_group_invoice_header_id
AND gid.is_deleted = false
AND ih.invoice_status_id IN (3,4)
AND ih.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
GROUP BY DATE_TRUNC('month', ih.invoice_date)
UNION ALL
SELECT row_number() OVER ()::int,
'Overdue',
COUNT(*)::int,
COALESCE(SUM(ih.total_amount - ih.settled_amount),0),
DATE_TRUNC('month', ih.invoice_date)::date
FROM group_invoice_details gid
JOIN invoice_headers ih ON ih.id = gid.invoice_header_id
WHERE gid.group_invoice_header_id = p_group_invoice_header_id
AND gid.is_deleted = false
AND ih.invoice_status_id IN (3,4)
AND ih.due_date < now()
AND ih.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
GROUP BY DATE_TRUNC('month', ih.invoice_date);
-- Case 2: Company-wide Analytics
ELSE
RETURN QUERY
SELECT row_number() OVER ()::int,
'Paid',
COUNT(*)::int,
COALESCE(SUM(i.settled_amount), 0),
DATE_TRUNC('month', i.invoice_date)::date
FROM invoice_headers i
WHERE i.company_id = p_company_id
AND i.invoice_status_id IN (4, 5)
AND i.is_deleted = false
AND i.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
GROUP BY DATE_TRUNC('month', i.invoice_date)
UNION ALL
SELECT row_number() OVER ()::int,
'Pending',
COUNT(*)::int,
COALESCE(SUM(i.total_amount - i.settled_amount), 0),
DATE_TRUNC('month', i.invoice_date)::date
FROM invoice_headers i
WHERE i.company_id = p_company_id
AND i.invoice_status_id IN (3, 4)
AND i.is_deleted = false
AND i.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
GROUP BY DATE_TRUNC('month', i.invoice_date)
UNION ALL
SELECT row_number() OVER ()::int,
'Overdue',
COUNT(*)::int,
COALESCE(SUM(i.total_amount - i.settled_amount), 0),
DATE_TRUNC('month', i.invoice_date)::date
FROM invoice_headers i
WHERE i.company_id = p_company_id
AND i.invoice_status_id IN (3, 4)
AND i.due_date < CURRENT_DATE
AND i.is_deleted = false
AND i.invoice_date BETWEEN v_financial_year_start AND v_financial_year_end
GROUP BY DATE_TRUNC('month', i.invoice_date);
END IF;
END;
$function$
-- Function: get_grouped_invoice_header_by_limit
CREATE OR REPLACE FUNCTION public.get_grouped_invoice_header_by_limit(p_company_id uuid, p_fin_year_id integer, p_limit integer DEFAULT NULL::integer)
RETURNS TABLE(id uuid, group_invoice_number text, grouped_invoice_template_id uuid, execution_date timestamp without time zone, error_message text, success_count integer, total_count integer, invoice_description text, group_invoice_batch_id text, paid_count integer, total_paid_amount numeric, original_total_amount numeric, remaining_amount numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
BEGIN
IF p_limit IS NOT NULL THEN
-- If p_limit is provided, apply the LIMIT
RETURN QUERY
SELECT
gih.id,
gih.group_invoice_number,
gih.group_invoice_template_id,
gih.execution_date,
gih.error_message,
gih.success_count,
gih.total_count,
templates.invoice_description,
gih.group_invoice_batch_id,
CAST(COALESCE(SUM(CASE WHEN ih.settled_amount >= ih.total_amount THEN 1 ELSE 0 END), 0) AS integer) AS paid_count,
COALESCE(SUM(ih.settled_amount), 0) AS total_paid_amount,
COALESCE(SUM(ih.total_amount), 0) AS original_total_amount,
COALESCE(SUM(ih.total_amount - ih.settled_amount), 0) AS remaining_amount
FROM
public.group_invoice_headers gih
INNER JOIN
public.group_invoice_templates templates
ON gih.group_invoice_template_id = templates.id
LEFT JOIN
public.group_invoice_details gid
ON gid.group_invoice_header_id = gih.id
AND gid.is_deleted = false
AND gid.company_id = p_company_id
LEFT JOIN
public.invoice_headers ih
ON ih.id = gid.invoice_header_id
AND ih.is_deleted = false
AND ih.company_id = p_company_id
WHERE
gih.company_id = p_company_id
AND gih.execution_date::date BETWEEN v_start_date AND v_end_date
GROUP BY
gih.id,
gih.group_invoice_number,
gih.group_invoice_template_id,
gih.execution_date,
gih.error_message,
gih.success_count,
gih.total_count,
templates.invoice_description,
gih.group_invoice_batch_id
ORDER BY
gih.execution_date DESC
LIMIT p_limit; -- Apply the LIMIT when p_limit is provided
ELSE
-- If p_limit is NULL, return all results for the specified company and financial year
RETURN QUERY
SELECT
gih.id,
gih.group_invoice_number,
gih.group_invoice_template_id,
gih.execution_date,
gih.error_message,
gih.success_count,
gih.total_count,
templates.invoice_description,
gih.group_invoice_batch_id,
CAST(COALESCE(SUM(CASE WHEN ih.settled_amount >= ih.total_amount THEN 1 ELSE 0 END), 0) AS integer) AS paid_count,
COALESCE(SUM(ih.settled_amount), 0) AS total_paid_amount,
COALESCE(SUM(ih.total_amount), 0) AS original_total_amount,
COALESCE(SUM(ih.total_amount - ih.settled_amount), 0) AS remaining_amount
FROM
public.group_invoice_headers gih
INNER JOIN
public.group_invoice_templates templates
ON gih.group_invoice_template_id = templates.id
LEFT JOIN
public.group_invoice_details gid
ON gid.group_invoice_header_id = gih.id
AND gid.is_deleted = false
AND gid.company_id = p_company_id
LEFT JOIN
public.invoice_headers ih
ON ih.id = gid.invoice_header_id
AND ih.is_deleted = false
AND ih.company_id = p_company_id
WHERE
gih.company_id = p_company_id
AND gih.execution_date::date BETWEEN v_start_date AND v_end_date
GROUP BY
gih.id,
gih.group_invoice_number,
gih.group_invoice_template_id,
gih.execution_date,
gih.error_message,
gih.success_count,
gih.total_count,
templates.invoice_description,
gih.group_invoice_batch_id
ORDER BY
gih.execution_date DESC;
END IF;
END;
$function$
-- Function: get_all_invoices_from_span
CREATE OR REPLACE FUNCTION public.get_all_invoices_from_span(p_company_id uuid, p_fin_year_id integer, p_user_id uuid, p_type_id integer DEFAULT NULL::integer, p_start_date timestamp without time zone DEFAULT NULL::timestamp without time zone, p_end_date timestamp without time zone DEFAULT NULL::timestamp without time zone)
RETURNS TABLE(id uuid, invoice_number character varying, type integer, invoice_date date, customer_id uuid, customer_name character varying, due_date date, total_amount numeric, settled_amount numeric, invoice_status_id integer, invoice_status character varying, currency_id integer, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, created_by uuid, modified_by uuid, created_by_name character varying, modified_by_name character varying, is_created_by_scheduler boolean, has_next_status_approval boolean, next_status_id integer, has_pending_payment_verification boolean)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := COALESCE(p_start_date::date, MAKE_DATE(p_fin_year_id, 4, 1));
v_end_date date := COALESCE(p_end_date::date, MAKE_DATE(p_fin_year_id + 1, 3, 31));
DRAFT_STATUS CONSTANT integer := 1;
v_organization_id uuid;
BEGIN
RETURN QUERY
WITH invoice_data AS (
SELECT
ih.id,
CAST(ih.invoice_number AS varchar) AS invoice_number,
ih.type,
ih.invoice_date::date,
ih.customer_id,
c.name AS customer_name,
ih.due_date::date,
ih.total_amount,
ih.settled_amount,
ih.invoice_status_id,
s.name AS invoice_status,
ih.currency_id,
ih.created_on_utc,
ih.modified_on_utc,
ih.created_by,
ih.modified_by,
CAST(CONCAT(cu.first_name, ' ', cu.last_name) AS varchar) AS created_by_name,
CAST(CONCAT(mu.first_name, ' ', mu.last_name) AS varchar) AS modified_by_name,
ih.is_created_by_scheduler,
ih.credit_account_id
FROM invoice_headers ih
LEFT JOIN customers c ON c.id = ih.customer_id
LEFT JOIN invoice_statuses s ON s.id = ih.invoice_status_id
LEFT JOIN users cu ON cu.id = ih.created_by
LEFT JOIN users mu ON mu.id = ih.modified_by
WHERE ih.company_id = p_company_id
AND ih.is_deleted = false
AND ih.invoice_date BETWEEN v_start_date AND v_end_date
AND (p_type_id IS NULL OR p_type_id = 0 OR ih.type = p_type_id)
UNION ALL
SELECT
dih.id,
CAST(dih.invoice_number AS varchar) AS invoice_number,
dih.type,
dih.invoice_date::date,
dih.customer_id,
c.name AS customer_name,
dih.due_date::date,
dih.total_amount,
dih.settled_amount,
dih.invoice_status_id,
s.name AS invoice_status,
dih.currency_id,
dih.created_on_utc,
dih.modified_on_utc,
dih.created_by,
dih.modified_by,
CAST(CONCAT(cu.first_name, ' ', cu.last_name) AS varchar) AS created_by_name,
CAST(CONCAT(mu.first_name, ' ', mu.last_name) AS varchar) AS modified_by_name,
dih.is_created_by_scheduler,
dih.credit_account_id
FROM draft_invoice_headers dih
LEFT JOIN customers c ON c.id = dih.customer_id
LEFT JOIN invoice_statuses s ON s.id = dih.invoice_status_id
LEFT JOIN users cu ON cu.id = dih.created_by
LEFT JOIN users mu ON mu.id = dih.modified_by
WHERE dih.company_id = p_company_id
AND dih.is_deleted = false
AND dih.invoice_date BETWEEN v_start_date AND v_end_date
AND (p_type_id IS NULL OR p_type_id = 0 OR dih.type = p_type_id)
)
SELECT
id.id,
id.invoice_number,
id.type,
id.invoice_date,
id.customer_id,
id.customer_name,
id.due_date,
id.total_amount,
id.settled_amount,
id.invoice_status_id,
id.invoice_status,
id.currency_id,
id.created_on_utc,
id.modified_on_utc,
id.created_by,
id.modified_by,
id.created_by_name,
id.modified_by_name,
id.is_created_by_scheduler,
-- Determine if current user can approve
CASE
WHEN id.invoice_status_id = DRAFT_STATUS THEN true
WHEN EXISTS (
SELECT 1
FROM public.invoice_approval_users_account a
WHERE a.account_id = id.credit_account_id
AND a.status_id = id.invoice_status_id
AND a.user_id = p_user_id
AND a.is_deleted = false
) THEN true
WHEN EXISTS (
SELECT 1
FROM public.invoice_approval_user_company c
WHERE c.company_id = p_company_id
AND c.status_id = id.invoice_status_id
AND c.user_id = p_user_id
AND c.is_deleted = false
) THEN true
ELSE false
END AS has_next_status_approval,
-- Determine next status
COALESCE(
(SELECT iw.next_status
FROM invoice_workflow iw
WHERE iw.company_id = p_company_id
AND iw.status = id.invoice_status_id
AND iw.is_deleted = false
LIMIT 1),
0
) AS next_status_id,
-- Check if there's a pending payment verification
EXISTS (
SELECT 1
FROM public.invoice_payment_headers iph
JOIN public.invoice_payment_details ipd
ON ipd.invoice_payment_header_id = iph.id
WHERE ipd.invoice_header_id = id.id
AND iph.payment_status_id = 4 -- Payment Pending Verification status
AND iph.is_deleted = false
) AS has_pending_payment_verification
FROM invoice_data id;
END;
$function$
-- Function: get_unit_dues_by_cust_id
CREATE OR REPLACE FUNCTION public.get_unit_dues_by_cust_id(p_company_id uuid, p_customer_id uuid, p_fin_year_id integer)
RETURNS TABLE(id uuid, invoice_number text, note text, due_date timestamp without time zone, total_due numeric, total_paid numeric, original_amount numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1); -- Default: April 1st of the given financial year
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31); -- Default: March 31st of the next financial year
BEGIN
RETURN QUERY
SELECT
i.id AS id,
i.invoice_number,
i.note,
i.due_date,
(i.total_amount - i.settled_amount) AS total_due,
i.settled_amount AS total_paid,
i.total_amount AS original_amount
FROM invoice_headers i
INNER JOIN customers c
ON c.id = i.customer_id
AND c.company_id = p_company_id
WHERE
i.company_id = p_company_id
AND i.customer_id = p_customer_id
AND c.is_deleted = false
AND i.is_deleted = false
AND i.invoice_status_id IN (3, 4) -- Approved / Partially Paid
AND (i.total_amount - i.settled_amount) > 0
AND i.due_date >= v_start_date -- Filter by the financial year start date
AND i.due_date <= v_end_date -- Filter by the financial year end date
ORDER BY i.due_date;
END;
$function$
-- Function: get_defaulter_list
CREATE OR REPLACE FUNCTION public.get_defaulter_list(p_company_id uuid, p_as_of_date timestamp without time zone, p_limit integer DEFAULT NULL::integer)
RETURNS TABLE(id integer, customer_id uuid, customer_name character varying, due_date date, days_overdue integer, total_outstanding_per_customer numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT *
FROM (
SELECT
ROW_NUMBER() OVER (
ORDER BY
SUM(ih.total_amount - ih.settled_amount) DESC,
MAX(DATE_PART('day', p_as_of_date - ih.due_date)) DESC,
c."name" ASC
)::INT AS id,
c.id AS customer_id,
c."name" AS customer_name,
-- Oldest unpaid invoice due date
MIN(ih.due_date)::DATE AS due_date,
-- Max overdue days across invoices
MAX(
DATE_PART('day', p_as_of_date - ih.due_date)
)::INT AS days_overdue,
SUM(ih.total_amount - ih.settled_amount)
AS total_outstanding_per_customer
FROM public.invoice_headers ih
JOIN public.customers c
ON c.id = ih.customer_id
LEFT JOIN public.defaulter_configurations dc
ON dc.company_id = p_company_id
WHERE
ih.settled_amount < ih.total_amount
AND ih.due_date < p_as_of_date
AND ih.is_deleted = FALSE
AND c.is_deleted = FALSE
AND ih.company_id = p_company_id
AND dc.is_deleted = FALSE
AND DATE_PART('day', p_as_of_date - ih.due_date)
> dc.defaulter_period_days
GROUP BY
c.id,
c."name"
) ranked
ORDER BY
ranked.id
LIMIT
COALESCE(p_limit, 2147483647);
END;
$function$
-- Function: get_unit_dues_summary_by_cust_id
CREATE OR REPLACE FUNCTION public.get_unit_dues_summary_by_cust_id(p_company_id uuid, p_customer_id uuid, p_fin_year_id integer)
RETURNS TABLE(last_collection_target_value numeric, last_collection_collected_value numeric, last_collection_units_targeted integer, last_collection_paid_units integer, total_outstanding_due numeric, total_units_not_paid integer)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
v_latest_cycle_date date;
BEGIN
/*
* 1) Find latest invoice cycle date within the FY
* Using invoice_date if present, otherwise created_on_utc
*/
SELECT MAX(COALESCE(i.invoice_date::date, i.created_on_utc::date))
INTO v_latest_cycle_date
FROM invoice_headers i
INNER JOIN customers c ON c.id = i.customer_id
WHERE i.company_id = p_company_id
AND i.customer_id = p_customer_id
AND c.company_id = p_company_id
AND c.is_deleted = false
AND i.is_deleted = false
AND i.invoice_status_id IN (3, 4) -- Approved / Partially Paid
AND i.due_date::date BETWEEN v_start_date AND v_end_date;
-- No invoices in FY → return zero summary
IF v_latest_cycle_date IS NULL THEN
RETURN QUERY
SELECT 0, 0, 0, 0, 0, 0;
RETURN;
END IF;
/*
* 2) Build FY invoice set
*/
RETURN QUERY
WITH fy_invoices AS (
SELECT
i.customer_id,
i.total_amount,
i.settled_amount,
(i.total_amount - i.settled_amount) AS due_amount,
COALESCE(i.invoice_date::date, i.created_on_utc::date) AS cycle_date
FROM invoice_headers i
INNER JOIN customers c ON c.id = i.customer_id
WHERE i.company_id = p_company_id
AND i.customer_id = p_customer_id
AND c.company_id = p_company_id
AND c.is_deleted = false
AND i.is_deleted = false
AND i.invoice_status_id IN (3, 4)
AND i.due_date::date BETWEEN v_start_date AND v_end_date
),
last_cycle AS (
SELECT *
FROM fy_invoices
WHERE cycle_date = v_latest_cycle_date
),
last_cycle_rollup AS (
SELECT
COALESCE(SUM(total_amount), 0) AS target_value,
COALESCE(SUM(settled_amount), 0) AS collected_value,
COUNT(DISTINCT customer_id)::int AS units_targeted,
COUNT(
DISTINCT CASE
WHEN due_amount <= 0 THEN customer_id
END
)::int AS paid_units
FROM last_cycle
),
overall_rollup AS (
SELECT
COALESCE(SUM(due_amount), 0) AS total_due,
COUNT(
DISTINCT CASE
WHEN due_amount > 0 THEN customer_id
END
)::int AS units_not_paid
FROM fy_invoices
)
SELECT
l.target_value,
l.collected_value,
l.units_targeted,
l.paid_units,
o.total_due,
o.units_not_paid
FROM last_cycle_rollup l
CROSS JOIN overall_rollup o;
END;
$function$
-- Function: get_unit_dues_summary_by_company
CREATE OR REPLACE FUNCTION public.get_unit_dues_summary_by_company(p_company_id uuid, p_fin_year_id integer)
RETURNS TABLE(last_collection_target_value numeric, last_collection_collected_value numeric, last_collection_units_targeted integer, last_collection_paid_units integer, total_outstanding_due numeric, total_units_not_paid integer)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := MAKE_DATE(p_fin_year_id, 4, 1);
v_end_date date := MAKE_DATE(p_fin_year_id + 1, 3, 31);
v_latest_cycle_date date;
BEGIN
/*
* 1) Find latest invoice cycle date in FY (company-wide)
*/
SELECT MAX(COALESCE(i.invoice_date::date, i.created_on_utc::date))
INTO v_latest_cycle_date
FROM invoice_headers i
INNER JOIN customers c ON c.id = i.customer_id
WHERE i.company_id = p_company_id
AND c.company_id = p_company_id
AND c.is_deleted = false
AND i.is_deleted = false
AND i.invoice_status_id IN (3, 4) -- Approved / Partially Paid
AND i.due_date::date BETWEEN v_start_date AND v_end_date;
-- No invoices in FY → return zero summary
IF v_latest_cycle_date IS NULL THEN
RETURN QUERY
SELECT 0, 0, 0, 0, 0, 0;
RETURN;
END IF;
/*
* 2) Build FY invoice dataset
*/
RETURN QUERY
WITH fy_invoices AS (
SELECT
i.customer_id,
i.total_amount,
i.settled_amount,
(i.total_amount - i.settled_amount) AS due_amount,
COALESCE(i.invoice_date::date, i.created_on_utc::date) AS cycle_date
FROM invoice_headers i
INNER JOIN customers c ON c.id = i.customer_id
WHERE i.company_id = p_company_id
AND c.company_id = p_company_id
AND c.is_deleted = false
AND i.is_deleted = false
AND i.invoice_status_id IN (3, 4)
AND i.due_date::date BETWEEN v_start_date AND v_end_date
),
last_cycle AS (
SELECT *
FROM fy_invoices
WHERE cycle_date = v_latest_cycle_date
),
last_cycle_rollup AS (
SELECT
COALESCE(SUM(total_amount), 0) AS target_value,
COALESCE(SUM(settled_amount), 0) AS collected_value,
COUNT(DISTINCT customer_id)::int AS units_targeted,
COUNT(
DISTINCT CASE
WHEN due_amount <= 0 THEN customer_id
END
)::int AS paid_units
FROM last_cycle
),
overall_rollup AS (
SELECT
COALESCE(SUM(due_amount), 0) AS total_due,
COUNT(
DISTINCT CASE
WHEN due_amount > 0 THEN customer_id
END
)::int AS units_not_paid
FROM fy_invoices
)
SELECT
l.target_value,
l.collected_value,
l.units_targeted,
l.paid_units,
o.total_due,
o.units_not_paid
FROM last_cycle_rollup l
CROSS JOIN overall_rollup o;
END;
$function$
-- Function: get_discussion_messages
CREATE OR REPLACE FUNCTION public.get_discussion_messages(p_organization_id uuid, p_cycle_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_result jsonb;
BEGIN
-- Verify the cycle exists and belongs to the organization
IF NOT EXISTS (
SELECT 1
FROM public.delinquency_cycles dc
WHERE dc.id = p_cycle_id
AND dc.organization_id = p_organization_id
AND dc.is_deleted = false
) THEN
RETURN jsonb_build_object('messages', jsonb_build_array());
END IF;
-- Fetch all messages for the cycle
SELECT jsonb_build_object(
'messages',
COALESCE(
jsonb_agg(row_to_json(msg)),
jsonb_build_array()
)
)
INTO v_result
FROM (
SELECT
ddm.id,
ddm.cycle_id,
ddm.sender_id,
concat(u.first_name, ' ', u.last_name) AS sender_name,
ddm.sender_type,
ddm.message_text,
ddm.sent_on_utc
FROM public.delinquency_discussion_messages ddm
LEFT JOIN public.users u
ON u.id = ddm.sender_id
WHERE ddm.cycle_id = p_cycle_id
AND ddm.organization_id = p_organization_id
AND ddm.is_deleted = false
ORDER BY ddm.sent_on_utc ASC
) msg;
RETURN v_result;
END;
$function$
-- Function: dblink_connect
CREATE OR REPLACE FUNCTION public.dblink_connect(text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_connect$function$
-- Function: dblink_connect
CREATE OR REPLACE FUNCTION public.dblink_connect(text, text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_connect$function$
-- Function: dblink_connect_u
CREATE OR REPLACE FUNCTION public.dblink_connect_u(text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT SECURITY DEFINER
AS '$libdir/dblink', $function$dblink_connect$function$
-- Function: dblink_connect_u
CREATE OR REPLACE FUNCTION public.dblink_connect_u(text, text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT SECURITY DEFINER
AS '$libdir/dblink', $function$dblink_connect$function$
-- Function: dblink_disconnect
CREATE OR REPLACE FUNCTION public.dblink_disconnect()
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_disconnect$function$
-- Function: dblink_disconnect
CREATE OR REPLACE FUNCTION public.dblink_disconnect(text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_disconnect$function$
-- Function: dblink_open
CREATE OR REPLACE FUNCTION public.dblink_open(text, text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_open$function$
-- Function: dblink_open
CREATE OR REPLACE FUNCTION public.dblink_open(text, text, boolean)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_open$function$
-- Function: dblink_open
CREATE OR REPLACE FUNCTION public.dblink_open(text, text, text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_open$function$
-- Function: dblink_open
CREATE OR REPLACE FUNCTION public.dblink_open(text, text, text, boolean)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_open$function$
-- Function: dblink_fetch
CREATE OR REPLACE FUNCTION public.dblink_fetch(text, integer)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_fetch$function$
-- Function: dblink_fetch
CREATE OR REPLACE FUNCTION public.dblink_fetch(text, integer, boolean)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_fetch$function$
-- Function: dblink_fetch
CREATE OR REPLACE FUNCTION public.dblink_fetch(text, text, integer)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_fetch$function$
-- Function: dblink_fetch
CREATE OR REPLACE FUNCTION public.dblink_fetch(text, text, integer, boolean)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_fetch$function$
-- Function: dblink_close
CREATE OR REPLACE FUNCTION public.dblink_close(text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_close$function$
-- Function: dblink_close
CREATE OR REPLACE FUNCTION public.dblink_close(text, boolean)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_close$function$
-- Function: dblink_close
CREATE OR REPLACE FUNCTION public.dblink_close(text, text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_close$function$
-- Function: dblink_close
CREATE OR REPLACE FUNCTION public.dblink_close(text, text, boolean)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_close$function$
-- Function: dblink
CREATE OR REPLACE FUNCTION public.dblink(text, text)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_record$function$
-- Function: dblink
CREATE OR REPLACE FUNCTION public.dblink(text, text, boolean)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_record$function$
-- Function: dblink
CREATE OR REPLACE FUNCTION public.dblink(text)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_record$function$
-- Function: dblink
CREATE OR REPLACE FUNCTION public.dblink(text, boolean)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_record$function$
-- Function: dblink_exec
CREATE OR REPLACE FUNCTION public.dblink_exec(text, text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_exec$function$
-- Function: dblink_exec
CREATE OR REPLACE FUNCTION public.dblink_exec(text, text, boolean)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_exec$function$
-- Function: dblink_exec
CREATE OR REPLACE FUNCTION public.dblink_exec(text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_exec$function$
-- Function: open_or_get_delinquency_cycle
CREATE OR REPLACE FUNCTION public.open_or_get_delinquency_cycle(p_organization_id uuid, p_customer_id uuid, p_start_due_date date, p_created_by uuid)
RETURNS uuid
LANGUAGE plpgsql
AS $function$
DECLARE
v_cycle_id uuid;
CYCLE_STATUS_OPEN CONSTANT int := 1;
BEGIN
-- Try to get existing open cycle
SELECT id
INTO v_cycle_id
FROM delinquency_cycles
WHERE organization_id = p_organization_id
AND customer_id = p_customer_id
AND ended_on_utc IS NULL
AND is_deleted = false
LIMIT 1;
IF v_cycle_id IS NOT NULL THEN
RETURN v_cycle_id;
END IF;
-- if not then Create new cycle
INSERT INTO delinquency_cycles (
id,
organization_id,
customer_id,
start_due_date,
started_on_utc,
status_id,
created_on_utc,
created_by,
is_deleted
)
VALUES (
gen_random_uuid(),
p_organization_id,
p_customer_id,
p_start_due_date,
CURRENT_TIMESTAMP,
CYCLE_STATUS_OPEN,
CURRENT_TIMESTAMP,
p_created_by,
false
)
RETURNING id INTO v_cycle_id;
RETURN v_cycle_id;
END;
$function$
-- Function: dblink_exec
CREATE OR REPLACE FUNCTION public.dblink_exec(text, boolean)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_exec$function$
-- Function: dblink_get_pkey
CREATE OR REPLACE FUNCTION public.dblink_get_pkey(text)
RETURNS SETOF dblink_pkey_results
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_get_pkey$function$
-- Function: dblink_build_sql_insert
CREATE OR REPLACE FUNCTION public.dblink_build_sql_insert(text, int2vector, integer, text[], text[])
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_build_sql_insert$function$
-- Function: dblink_build_sql_delete
CREATE OR REPLACE FUNCTION public.dblink_build_sql_delete(text, int2vector, integer, text[])
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_build_sql_delete$function$
-- Function: dblink_build_sql_update
CREATE OR REPLACE FUNCTION public.dblink_build_sql_update(text, int2vector, integer, text[], text[])
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_build_sql_update$function$
-- Function: dblink_current_query
CREATE OR REPLACE FUNCTION public.dblink_current_query()
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED
AS '$libdir/dblink', $function$dblink_current_query$function$
-- Function: dblink_send_query
CREATE OR REPLACE FUNCTION public.dblink_send_query(text, text)
RETURNS integer
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_send_query$function$
-- Function: dblink_is_busy
CREATE OR REPLACE FUNCTION public.dblink_is_busy(text)
RETURNS integer
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_is_busy$function$
-- Function: dblink_get_result
CREATE OR REPLACE FUNCTION public.dblink_get_result(text)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_get_result$function$
-- Function: dblink_get_result
CREATE OR REPLACE FUNCTION public.dblink_get_result(text, boolean)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_get_result$function$
-- Function: dblink_get_connections
CREATE OR REPLACE FUNCTION public.dblink_get_connections()
RETURNS text[]
LANGUAGE c
PARALLEL RESTRICTED
AS '$libdir/dblink', $function$dblink_get_connections$function$
-- Function: dblink_cancel_query
CREATE OR REPLACE FUNCTION public.dblink_cancel_query(text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_cancel_query$function$
-- Function: dblink_error_message
CREATE OR REPLACE FUNCTION public.dblink_error_message(text)
RETURNS text
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_error_message$function$
-- Function: dblink_get_notify
CREATE OR REPLACE FUNCTION public.dblink_get_notify(OUT notify_name text, OUT be_pid integer, OUT extra text)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_get_notify$function$
-- Function: dblink_get_notify
CREATE OR REPLACE FUNCTION public.dblink_get_notify(conname text, OUT notify_name text, OUT be_pid integer, OUT extra text)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/dblink', $function$dblink_get_notify$function$
-- Function: dblink_fdw_validator
CREATE OR REPLACE FUNCTION public.dblink_fdw_validator(options text[], catalog oid)
RETURNS void
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/dblink', $function$dblink_fdw_validator$function$
-- Function: get_latest_raised_group_invoice_summary
CREATE OR REPLACE FUNCTION public.get_latest_raised_group_invoice_summary(p_company_id uuid DEFAULT NULL::uuid)
RETURNS TABLE(id integer, group_invoice_id uuid, group_invoice_number text, total_targeted_revenue numeric, collected_revenue numeric, total_units_count integer, collected_units_count integer)
LANGUAGE sql
STABLE
AS $function$
WITH latest_raised_group_invoice AS (
SELECT
gih.id,
gih.group_invoice_number
FROM public.group_invoice_headers gih
WHERE gih.is_deleted = false
AND (
p_company_id IS NULL
OR gih.company_id = p_company_id
)
AND EXISTS (
SELECT 1
FROM public.group_invoice_details gid
WHERE gid.group_invoice_header_id = gih.id
AND gid.is_deleted = false
)
ORDER BY gih.created_on_utc DESC NULLS LAST
LIMIT 1
)
SELECT
1 AS id,
lgi.id AS group_invoice_id,
lgi.group_invoice_number,
SUM(ih.total_amount) AS total_targeted_revenue,
SUM(COALESCE(ih.settled_amount, 0)) AS collected_revenue,
COUNT(DISTINCT gid.unit_id) AS total_units_count,
COUNT(
DISTINCT CASE
WHEN COALESCE(ih.settled_amount, 0) > 0
THEN gid.unit_id
END
) AS collected_units_count
FROM latest_raised_group_invoice lgi
JOIN public.group_invoice_details gid
ON gid.group_invoice_header_id = lgi.id
AND gid.is_deleted = false
JOIN public.invoice_headers ih
ON ih.id = gid.invoice_header_id
AND ih.is_deleted = false
GROUP BY
lgi.id,
lgi.group_invoice_number;
$function$
-- Function: get_aging_bucket
CREATE OR REPLACE FUNCTION public.get_aging_bucket(p_days integer)
RETURNS text
LANGUAGE sql
IMMUTABLE
AS $function$
SELECT CASE
WHEN p_days <= 30 THEN '0-30'
WHEN p_days <= 60 THEN '31-60'
WHEN p_days <= 90 THEN '61-90'
ELSE '90+'
END;
$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_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_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: run_defaulter_fdw
CREATE OR REPLACE FUNCTION public.run_defaulter_fdw()
RETURNS TABLE(result text)
LANGUAGE plpgsql
AS $function$
BEGIN
PERFORM public.evaluate_delinquency_for_org('b2756bc9-be6a-42bb-a78a-178ea22b46eb');
RETURN QUERY SELECT 'SUCCESS';
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,
'00000000-0000-0000-0000-000000000000'::uuid
);
END;
$function$
-- Function: get_invoice_payment_timeline_by_customerid
CREATE OR REPLACE FUNCTION public.get_invoice_payment_timeline_by_customerid(p_customer_id uuid, p_company_id uuid DEFAULT NULL::uuid)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
result jsonb;
BEGIN
SELECT jsonb_agg(
jsonb_build_object(
'section', section,
'dates', dates
)
)
INTO result
FROM (
SELECT
section,
jsonb_agg(date_block ORDER BY date_block->>'date') AS dates
FROM (
SELECT
section,
jsonb_build_object(
'date', event_date,
'dateLabel', to_char(event_date, 'Mon DD'),
'items',
jsonb_agg(
jsonb_build_object(
'eventType', event_type,
'invoiceNumber', invoice_number,
'paymentNumber', payment_number,
'reference', reference,
'title', title,
'timeLabel', time_label,
'status', status,
'amount', amount,
'daysToDue', days_to_due,
'dueLabel', due_label,
'invoiceId', invoice_id,
'paymentId', payment_id
)
ORDER BY event_time
)
) AS date_block
FROM (
/* =====================================================
INVOICE ISSUED (to resident)
====================================================== */
SELECT
ih.id AS invoice_id,
NULL::uuid AS payment_id,
ih.invoice_number AS invoice_number,
NULL::text AS payment_number,
ih.created_on_utc AS event_time,
ih.created_on_utc::date AS event_date,
'Invoice' AS event_type,
ih.invoice_number AS reference,
'Invoice Issued' AS title,
to_char(ih.created_on_utc, 'HH12:MI AM') AS time_label,
ih.total_amount AS amount,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN 'PAID'
WHEN ih.settled_amount > 0 THEN 'PARTIAL'
WHEN ih.due_date < now() THEN 'OVERDUE'
ELSE 'DUE'
END AS status,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN NULL
ELSE (ih.due_date::date - current_date)
END AS days_to_due,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN NULL
WHEN ih.due_date::date - current_date < 0
THEN concat('Overdue by ', abs(ih.due_date::date - current_date), ' days')
ELSE concat('Due in ', ih.due_date::date - current_date, ' days')
END AS due_label,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN 'Settled'
ELSE 'Open'
END AS section
FROM invoice_headers ih
WHERE ih.customer_id = p_customer_id
AND ih.is_deleted = false
AND (p_company_id IS NULL OR ih.company_id = p_company_id)
UNION ALL
/* =====================================================
PAYMENT MADE (by resident)
====================================================== */
SELECT
ipd.invoice_header_id AS invoice_id,
iph.id AS payment_id,
ih.invoice_number AS invoice_number,
iph.payment_number AS payment_number,
iph.received_date AS event_time,
iph.received_date::date AS event_date,
'Payment' AS event_type,
iph.payment_number AS reference,
CASE
WHEN ih.settled_amount >= ih.total_amount
THEN 'Invoice Paid'
ELSE 'Partial Payment Made'
END AS title,
to_char(iph.received_date, 'HH12:MI AM') AS time_label,
ipd.received_amount AS amount,
'PAYMENT' AS status,
NULL::int AS days_to_due,
NULL::text AS due_label,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN 'Settled'
ELSE 'Open'
END AS section
FROM invoice_payment_details ipd
JOIN invoice_payment_headers iph
ON iph.id = ipd.invoice_payment_header_id
AND iph.is_deleted = false
JOIN invoice_headers ih
ON ih.id = ipd.invoice_header_id
AND ih.is_deleted = false
WHERE iph.customer_id = p_customer_id
AND ipd.is_deleted = false
AND (p_company_id IS NULL OR iph.company_id = p_company_id)
) timeline_events
GROUP BY section, event_date
) grouped_dates
GROUP BY section
) final_sections;
RETURN COALESCE(result, '[]'::jsonb);
END;
$function$
-- Function: get_invoice_payment_timeline_by_customerid
CREATE OR REPLACE FUNCTION public.get_invoice_payment_timeline_by_customerid(p_customer_id uuid, p_company_id uuid, p_financial_year_start integer)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
result jsonb;
v_financial_year_start date;
v_financial_year_end date;
BEGIN
/* ---------------------------------------------------------
Financial Year Window (India: Apr 1 → Mar 31)
--------------------------------------------------------- */
v_financial_year_start := TO_DATE(p_financial_year_start || '-04-01', 'YYYY-MM-DD');
v_financial_year_end := TO_DATE((p_financial_year_start + 1) || '-03-31', 'YYYY-MM-DD');
/* ---------------------------------------------------------
Timeline JSON
--------------------------------------------------------- */
SELECT jsonb_agg(
jsonb_build_object(
'section', section,
'dates', dates
)
)
INTO result
FROM (
SELECT
section,
jsonb_agg(date_block ORDER BY date_block->>'date') AS dates
FROM (
SELECT
section,
jsonb_build_object(
'date', event_date,
'dateLabel', to_char(event_date, 'Mon DD'),
'items',
jsonb_agg(
jsonb_build_object(
'eventType', event_type,
'invoiceNumber', invoice_number,
'paymentNumber', payment_number,
'reference', reference,
'title', title,
'timeLabel', time_label,
'status', status,
'amount', amount,
'daysToDue', days_to_due,
'dueLabel', due_label,
'invoiceId', invoice_id,
'paymentId', payment_id
)
ORDER BY event_time
)
) AS date_block
FROM (
/* =====================================================
INVOICE ISSUED (Apartment → Resident)
====================================================== */
SELECT
ih.id AS invoice_id,
NULL::uuid AS payment_id,
ih.invoice_number,
NULL::text AS payment_number,
ih.created_on_utc AS event_time,
ih.due_date::date AS event_date,
'Invoice' AS event_type,
ih.invoice_number AS reference,
'Invoice Issued' AS title,
to_char(ih.created_on_utc, 'HH12:MI AM') AS time_label,
ih.total_amount AS amount,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN 'PAID'
WHEN ih.settled_amount > 0 THEN 'PARTIAL'
WHEN ih.due_date < current_date THEN 'OVERDUE'
ELSE 'DUE'
END AS status,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN NULL
ELSE (ih.due_date::date - current_date)
END AS days_to_due,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN NULL
WHEN ih.due_date::date < current_date
THEN 'Overdue by ' || abs(ih.due_date::date - current_date) || ' days'
ELSE 'Due in ' || (ih.due_date::date - current_date) || ' days'
END AS due_label,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN 'Settled'
ELSE 'Open'
END AS section
FROM invoice_headers ih
WHERE ih.customer_id = p_customer_id
AND ih.company_id = p_company_id
AND ih.is_deleted = false
AND ih.due_date::date BETWEEN v_financial_year_start AND v_financial_year_end
UNION ALL
/* =====================================================
PAYMENT MADE (Resident → Apartment)
====================================================== */
SELECT
ipd.invoice_header_id AS invoice_id,
iph.id AS payment_id,
ih.invoice_number,
iph.payment_number,
iph.received_date AS event_time,
iph.received_date::date AS event_date,
'Payment' AS event_type,
iph.payment_number AS reference,
CASE
WHEN ih.settled_amount >= ih.total_amount
THEN 'Invoice Paid'
ELSE 'Partial Payment Made'
END AS title,
to_char(iph.received_date, 'HH12:MI AM') AS time_label,
ipd.received_amount AS amount,
'PAYMENT' AS status,
NULL::int AS days_to_due,
NULL::text AS due_label,
CASE
WHEN ih.settled_amount >= ih.total_amount THEN 'Settled'
ELSE 'Open'
END AS section
FROM invoice_payment_details ipd
JOIN invoice_payment_headers iph
ON iph.id = ipd.invoice_payment_header_id
AND iph.is_deleted = false
JOIN invoice_headers ih
ON ih.id = ipd.invoice_header_id
AND ih.is_deleted = false
WHERE iph.customer_id = p_customer_id
AND iph.company_id = p_company_id
AND ipd.is_deleted = false
AND iph.received_date::date BETWEEN v_financial_year_start AND v_financial_year_end
) timeline_events
GROUP BY section, event_date
) grouped_dates
GROUP BY section
) final_sections;
RETURN COALESCE(result, '[]'::jsonb);
END;
$function$
-- Function: evaluate_delinquency_for_org
CREATE OR REPLACE FUNCTION public.evaluate_delinquency_for_org(p_organization_id uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
v_grace_days integer := 0;
v_company_ids uuid[];
v_system_user_id uuid;
BEGIN
/* ====================================================
1) Load latest collection policy
==================================================== */
SELECT COALESCE(cp.grace_days, 0)
INTO v_grace_days
FROM public.collection_policies cp
WHERE cp.organization_id = p_organization_id
AND cp.is_deleted = false
ORDER BY cp.created_on_utc DESC
LIMIT 1;
/* ====================================================
2) Fetch companies for organization
==================================================== */
SELECT COALESCE(array_agg(c.id), ARRAY[]::uuid[])
INTO v_company_ids
FROM public.companies c
WHERE c.organization_id = p_organization_id
AND c.is_deleted = false;
IF array_length(v_company_ids, 1) IS NULL THEN
RETURN;
END IF;
/* ====================================================
3) Resolve system user
==================================================== */
v_system_user_id := public.get_system_user_id();
/* ====================================================
4) Cleanup temp tables (safe for same session)
==================================================== */
DROP TABLE IF EXISTS tmp_overdue_invoices;
DROP TABLE IF EXISTS tmp_calc;
DROP TABLE IF EXISTS tmp_open_cycles;
DROP TABLE IF EXISTS tmp_cycles_created;
DROP TABLE IF EXISTS tmp_cycle_map;
DROP TABLE IF EXISTS tmp_snapshot_inserted;
DROP TABLE IF EXISTS tmp_active_snapshots;
/* ====================================================
5) MATERIALIZE overdue invoices
==================================================== */
CREATE TEMP TABLE tmp_overdue_invoices ON COMMIT DROP AS
SELECT
ih.company_id,
ih.customer_id,
ih.id AS invoice_id,
ih.invoice_date,
ih.due_date::date AS due_date,
ih.total_amount,
COALESCE(ih.settled_amount, 0) AS settled_amount,
(ih.total_amount - COALESCE(ih.settled_amount, 0)) AS outstanding_amount
FROM public.invoice_headers ih
WHERE ih.company_id = ANY (v_company_ids)
AND ih.is_deleted = false
AND (ih.total_amount - COALESCE(ih.settled_amount, 0)) > 0
AND (ih.due_date::date + v_grace_days) < CURRENT_DATE;
/* ====================================================
6) Aggregate delinquency per customer
==================================================== */
CREATE TEMP TABLE tmp_calc ON COMMIT DROP AS
SELECT
customer_id,
MIN(due_date) AS oldest_due_date,
SUM(outstanding_amount) AS overdue_amount,
(CURRENT_DATE - MIN(due_date + v_grace_days))::int AS days_past_due,
public.get_aging_bucket(
(CURRENT_DATE - MIN(due_date + v_grace_days))::int
) AS aging_bucket
FROM tmp_overdue_invoices
GROUP BY customer_id;
/* ====================================================
7) Existing open cycles
==================================================== */
CREATE TEMP TABLE tmp_open_cycles ON COMMIT DROP AS
SELECT
dc.customer_id,
dc.id AS cycle_id
FROM public.delinquency_cycles dc
WHERE dc.organization_id = p_organization_id
AND dc.ended_on_utc IS NULL
AND dc.is_deleted = false;
/* ====================================================
8) Create missing cycles (CTE bridge)
==================================================== */
CREATE TEMP TABLE tmp_cycles_created ON COMMIT DROP AS
WITH ins AS (
INSERT INTO public.delinquency_cycles (
id, organization_id, customer_id,
start_due_date, started_on_utc,
ended_on_utc,
status_id, resolution_state_id,
created_by, created_on_utc, is_deleted
)
SELECT
gen_random_uuid(),
p_organization_id,
c.customer_id,
c.oldest_due_date,
now(),
NULL,
1, -- status id
1, -- resolution status id 1
v_system_user_id,
now(),
false
FROM tmp_calc c
LEFT JOIN tmp_open_cycles oc ON oc.customer_id = c.customer_id
WHERE oc.cycle_id IS NULL
RETURNING customer_id, id AS cycle_id
)
SELECT * FROM ins;
/* ====================================================
9) Cycle map (existing + created)
==================================================== */
CREATE TEMP TABLE tmp_cycle_map ON COMMIT DROP AS
SELECT
c.customer_id,
COALESCE(oc.cycle_id, cc.cycle_id) AS cycle_id,
c.oldest_due_date,
c.overdue_amount,
c.days_past_due,
c.aging_bucket
FROM tmp_calc c
LEFT JOIN tmp_open_cycles oc ON oc.customer_id = c.customer_id
LEFT JOIN tmp_cycles_created cc ON cc.customer_id = c.customer_id;
/* ====================================================
10) Insert snapshot if missing (CTE bridge)
==================================================== */
CREATE TEMP TABLE tmp_snapshot_inserted ON COMMIT DROP AS
WITH ins AS (
INSERT INTO public.delinquency_snapshots (
id, organization_id, customer_id, cycle_id,
overdue_amount, oldest_due_date,
days_past_due, aging_bucket,
last_evaluated_on,
created_by, created_on_utc,
is_deleted
)
SELECT
gen_random_uuid(),
p_organization_id,
cm.customer_id,
cm.cycle_id,
cm.overdue_amount,
cm.oldest_due_date,
cm.days_past_due,
cm.aging_bucket,
now(),
v_system_user_id,
now(),
false
FROM tmp_cycle_map cm
WHERE NOT EXISTS (
SELECT 1
FROM public.delinquency_snapshots ds
WHERE ds.cycle_id = cm.cycle_id
AND ds.is_deleted = false
)
RETURNING cycle_id, id AS snapshot_id
)
SELECT * FROM ins;
/* ====================================================
11) Resolve active snapshot per cycle
==================================================== */
CREATE TEMP TABLE tmp_active_snapshots ON COMMIT DROP AS
SELECT
cm.customer_id,
cm.cycle_id,
COALESCE(
tsi.snapshot_id,
(
SELECT ds.id
FROM public.delinquency_snapshots ds
WHERE ds.cycle_id = cm.cycle_id
AND ds.is_deleted = false
ORDER BY ds.created_on_utc DESC
LIMIT 1
)
) AS snapshot_id
FROM tmp_cycle_map cm
LEFT JOIN tmp_snapshot_inserted tsi ON tsi.cycle_id = cm.cycle_id;
/* ====================================================
12) Update active snapshots
==================================================== */
UPDATE public.delinquency_snapshots ds
SET overdue_amount = c.overdue_amount,
oldest_due_date = c.oldest_due_date,
days_past_due = c.days_past_due,
aging_bucket = c.aging_bucket,
last_evaluated_on = now(),
modified_on_utc = now(),
modified_by = v_system_user_id
FROM tmp_cycle_map c
JOIN tmp_active_snapshots a ON a.cycle_id = c.cycle_id
WHERE ds.id = a.snapshot_id
AND ds.is_deleted = false;
/* ====================================================
13) Refresh snapshot invoices (UPSERT – idempotent)
==================================================== */
-- Soft delete old rows
UPDATE public.delinquency_snapshot_invoices dsi
SET is_deleted = true,
deleted_on_utc = now(),
modified_on_utc = now(),
modified_by = v_system_user_id
WHERE dsi.organization_id = p_organization_id
AND dsi.is_deleted = false
AND EXISTS (
SELECT 1
FROM tmp_active_snapshots a
WHERE a.snapshot_id = dsi.snapshot_id
);
-- Insert / revive latest overdue invoices
INSERT INTO public.delinquency_snapshot_invoices (
id,
organization_id,
company_id,
snapshot_id,
invoice_id,
invoice_date,
due_date,
total_amount,
settled_amount,
outstanding_amount,
created_on_utc,
created_by,
modified_on_utc,
modified_by,
is_deleted,
deleted_on_utc
)
SELECT
gen_random_uuid(),
p_organization_id,
oi.company_id,
a.snapshot_id,
oi.invoice_id,
oi.invoice_date,
oi.due_date,
oi.total_amount,
oi.settled_amount,
oi.outstanding_amount,
now(),
v_system_user_id,
now(),
v_system_user_id,
false,
NULL
FROM tmp_overdue_invoices oi
JOIN tmp_active_snapshots a
ON a.customer_id = oi.customer_id
GROUP BY
oi.company_id,
a.snapshot_id,
oi.invoice_id,
oi.invoice_date,
oi.due_date,
oi.total_amount,
oi.settled_amount,
oi.outstanding_amount
ON CONFLICT (snapshot_id, invoice_id)
DO UPDATE
SET
invoice_date = EXCLUDED.invoice_date,
due_date = EXCLUDED.due_date,
total_amount = EXCLUDED.total_amount,
settled_amount = EXCLUDED.settled_amount,
outstanding_amount = EXCLUDED.outstanding_amount,
is_deleted = false,
deleted_on_utc = NULL,
modified_on_utc = now(),
modified_by = v_system_user_id;
/* ====================================================
14) Close cycles with no overdue invoices
==================================================== */
UPDATE public.delinquency_cycles dc
SET ended_on_utc = now(),
status_id = 2,
modified_on_utc = now(),
modified_by = v_system_user_id
WHERE dc.organization_id = p_organization_id
AND dc.ended_on_utc IS NULL
AND dc.is_deleted = false
AND NOT EXISTS (
SELECT 1
FROM tmp_calc c
WHERE c.customer_id = dc.customer_id
);
/* ====================================================
15) Cleanup snapshots & windows for closed cycles
==================================================== */
UPDATE public.delinquency_snapshots ds
SET is_deleted = true,
deleted_on_utc = now(),
modified_on_utc = now(),
modified_by = v_system_user_id
WHERE ds.organization_id = p_organization_id
AND ds.is_deleted = false
AND EXISTS (
SELECT 1
FROM public.delinquency_cycles dc
WHERE dc.id = ds.cycle_id
AND dc.ended_on_utc IS NOT NULL
AND dc.is_deleted = false
);
UPDATE public.delinquency_resolution_window drw
SET is_deleted = true,
deleted_on_utc = now(),
modified_on_utc = now(),
modified_by = v_system_user_id
WHERE drw.is_deleted = false
AND EXISTS (
SELECT 1
FROM public.delinquency_cycles dc
WHERE dc.id = drw.id
AND dc.organization_id = p_organization_id
AND dc.ended_on_utc IS NOT NULL
AND dc.is_deleted = false
);
END;
$function$
-- Function: evaluate_delinquency_for_org
CREATE OR REPLACE FUNCTION public.evaluate_delinquency_for_org(p_organization_id uuid, p_company_id uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
v_grace_days integer := 0;
v_system_user_id uuid;
BEGIN
/* ====================================================
1) Load latest collection policy
==================================================== */
SELECT COALESCE(cp.grace_days, 0)
INTO v_grace_days
FROM public.collection_policies cp
WHERE cp.organization_id = p_organization_id
AND cp.is_deleted = false
ORDER BY cp.created_on_utc DESC
LIMIT 1;
v_system_user_id := public.get_system_user_id();
/* ====================================================
4) Cleanup temp tables (safe for same session)
==================================================== */
DROP TABLE IF EXISTS tmp_overdue_invoices;
DROP TABLE IF EXISTS tmp_calc;
DROP TABLE IF EXISTS tmp_open_cycles;
DROP TABLE IF EXISTS tmp_cycles_created;
DROP TABLE IF EXISTS tmp_cycle_map;
DROP TABLE IF EXISTS tmp_snapshot_inserted;
DROP TABLE IF EXISTS tmp_active_snapshots;
/* ====================================================
5) MATERIALIZE overdue invoices
==================================================== */
CREATE TEMP TABLE tmp_overdue_invoices ON COMMIT DROP AS
SELECT
company_id,
customer_id,
invoice_id,
invoice_date,
due_date,
total_amount,
settled_amount,
outstanding_amount
FROM public.get_canonical_overdue_invoices(p_company_id);
/* ====================================================
6) Aggregate delinquency per customer
==================================================== */
CREATE TEMP TABLE tmp_calc ON COMMIT DROP AS
SELECT
customer_id,
MIN(due_date) AS oldest_due_date,
SUM(outstanding_amount) AS overdue_amount,
(CURRENT_DATE - MIN(due_date + v_grace_days))::int AS days_past_due,
public.get_aging_bucket(
(CURRENT_DATE - MIN(due_date + v_grace_days))::int
) AS aging_bucket
FROM tmp_overdue_invoices
GROUP BY customer_id;
/* ====================================================
7) Existing open cycles
==================================================== */
CREATE TEMP TABLE tmp_open_cycles ON COMMIT DROP AS
SELECT
dc.customer_id,
dc.id AS cycle_id
FROM public.delinquency_cycles dc
WHERE dc.company_id = p_company_id
AND dc.ended_on_utc IS NULL
AND dc.is_deleted = false;
/* ====================================================
8) Create missing cycles (CTE bridge)
==================================================== */
CREATE TEMP TABLE tmp_cycles_created ON COMMIT DROP AS
WITH ins AS (
INSERT INTO public.delinquency_cycles (
id, organization_id, company_id, customer_id,
start_due_date, started_on_utc,
ended_on_utc,
status_id, resolution_state_id,
created_by, created_on_utc, is_deleted
)
SELECT
gen_random_uuid(),
p_organization_id,
p_company_id,
c.customer_id,
c.oldest_due_date,
now(),
NULL,
1, -- status id
1, -- resolution status id 1
v_system_user_id,
now(),
false
FROM tmp_calc c
LEFT JOIN tmp_open_cycles oc ON oc.customer_id = c.customer_id
WHERE oc.cycle_id IS NULL
RETURNING customer_id, id AS cycle_id
)
SELECT * FROM ins;
/* ====================================================
9) Cycle map (existing + created)
==================================================== */
CREATE TEMP TABLE tmp_cycle_map ON COMMIT DROP AS
SELECT
c.customer_id,
COALESCE(oc.cycle_id, cc.cycle_id) AS cycle_id,
c.oldest_due_date,
c.overdue_amount,
c.days_past_due,
c.aging_bucket
FROM tmp_calc c
LEFT JOIN tmp_open_cycles oc ON oc.customer_id = c.customer_id
LEFT JOIN tmp_cycles_created cc ON cc.customer_id = c.customer_id;
/* ====================================================
10) Insert snapshot if missing (CTE bridge)
==================================================== */
CREATE TEMP TABLE tmp_snapshot_inserted ON COMMIT DROP AS
WITH ins AS (
INSERT INTO public.delinquency_snapshots (
id, organization_id, company_id, customer_id, cycle_id,
overdue_amount, oldest_due_date,
days_past_due, aging_bucket,
last_evaluated_on,
created_by, created_on_utc,
is_deleted
)
SELECT
gen_random_uuid(),
p_organization_id,
p_company_id,
cm.customer_id,
cm.cycle_id,
cm.overdue_amount,
cm.oldest_due_date,
cm.days_past_due,
cm.aging_bucket,
now(),
v_system_user_id,
now(),
false
FROM tmp_cycle_map cm
WHERE NOT EXISTS (
SELECT 1
FROM public.delinquency_snapshots ds
WHERE ds.cycle_id = cm.cycle_id
AND ds.is_deleted = false
)
RETURNING cycle_id, id AS snapshot_id
)
SELECT * FROM ins;
/* ====================================================
11) Resolve active snapshot per cycle
==================================================== */
CREATE TEMP TABLE tmp_active_snapshots ON COMMIT DROP AS
SELECT
cm.customer_id,
cm.cycle_id,
COALESCE(
tsi.snapshot_id,
(
SELECT ds.id
FROM public.delinquency_snapshots ds
WHERE ds.cycle_id = cm.cycle_id
AND ds.is_deleted = false
ORDER BY ds.created_on_utc DESC
LIMIT 1
)
) AS snapshot_id
FROM tmp_cycle_map cm
LEFT JOIN tmp_snapshot_inserted tsi ON tsi.cycle_id = cm.cycle_id;
/* ====================================================
12) Update active snapshots
==================================================== */
UPDATE public.delinquency_snapshots ds
SET overdue_amount = c.overdue_amount,
oldest_due_date = c.oldest_due_date,
days_past_due = c.days_past_due,
aging_bucket = c.aging_bucket,
last_evaluated_on = now(),
modified_on_utc = now(),
modified_by = v_system_user_id
FROM tmp_cycle_map c
JOIN tmp_active_snapshots a ON a.cycle_id = c.cycle_id
WHERE ds.id = a.snapshot_id
AND ds.is_deleted = false;
/* ====================================================
13) Refresh snapshot invoices (UPSERT – idempotent)
==================================================== */
-- Soft delete old rows
UPDATE public.delinquency_snapshot_invoices dsi
SET is_deleted = true,
deleted_on_utc = now(),
modified_on_utc = now(),
modified_by = v_system_user_id
WHERE dsi.company_id = p_company_id
AND dsi.is_deleted = false
AND EXISTS (
SELECT 1
FROM tmp_active_snapshots a
WHERE a.snapshot_id = dsi.snapshot_id
);
-- Insert / revive latest overdue invoices
INSERT INTO public.delinquency_snapshot_invoices (
id,
company_id,
snapshot_id,
invoice_id,
invoice_date,
due_date,
total_amount,
settled_amount,
outstanding_amount,
created_on_utc,
created_by,
modified_on_utc,
modified_by,
is_deleted,
deleted_on_utc
)
SELECT
gen_random_uuid(),
oi.company_id,
a.snapshot_id,
oi.invoice_id,
oi.invoice_date,
oi.due_date,
oi.total_amount,
oi.settled_amount,
oi.outstanding_amount,
now(),
v_system_user_id,
now(),
v_system_user_id,
false,
NULL
FROM tmp_overdue_invoices oi
JOIN tmp_active_snapshots a
ON a.customer_id = oi.customer_id
GROUP BY
oi.company_id,
a.snapshot_id,
oi.invoice_id,
oi.invoice_date,
oi.due_date,
oi.total_amount,
oi.settled_amount,
oi.outstanding_amount
ON CONFLICT (snapshot_id, invoice_id)
DO UPDATE
SET
invoice_date = EXCLUDED.invoice_date,
due_date = EXCLUDED.due_date,
total_amount = EXCLUDED.total_amount,
settled_amount = EXCLUDED.settled_amount,
outstanding_amount = EXCLUDED.outstanding_amount,
is_deleted = false,
deleted_on_utc = NULL,
modified_on_utc = now(),
modified_by = v_system_user_id;
/* ====================================================
14) Close cycles with no overdue invoices
==================================================== */
UPDATE public.delinquency_cycles dc
SET ended_on_utc = now(),
status_id = 2,
modified_on_utc = now(),
modified_by = v_system_user_id
WHERE dc.organization_id = p_organization_id
AND dc.company_id = p_company_id
AND dc.ended_on_utc IS NULL
AND dc.is_deleted = false
AND NOT EXISTS (
SELECT 1
FROM tmp_calc c
WHERE c.customer_id = dc.customer_id
);
/* ====================================================
15) Cleanup snapshots & windows for closed cycles
==================================================== */
UPDATE public.delinquency_snapshots ds
SET is_deleted = true,
deleted_on_utc = now(),
modified_on_utc = now(),
modified_by = v_system_user_id
WHERE ds.organization_id = p_organization_id
AND ds.company_id = p_company_id
AND ds.is_deleted = false
AND EXISTS (
SELECT 1
FROM public.delinquency_cycles dc
WHERE dc.id = ds.cycle_id
AND dc.ended_on_utc IS NOT NULL
AND dc.is_deleted = false
);
UPDATE public.delinquency_resolution_window drw
SET is_deleted = true,
deleted_on_utc = now(),
modified_on_utc = now(),
modified_by = v_system_user_id
WHERE drw.is_deleted = false
AND EXISTS (
SELECT 1
FROM public.delinquency_cycles dc
WHERE dc.id = drw.id
AND dc.organization_id = p_organization_id
AND dc.company_id = p_company_id
AND dc.ended_on_utc IS NOT NULL
AND dc.is_deleted = false
);
END;
$function$
-- Function: create_delinquency_action
CREATE OR REPLACE FUNCTION public.create_delinquency_action(p_organization_id uuid, p_customer_id uuid, p_cycle_id uuid, p_action_type_code text, p_action_on_utc timestamp with time zone, p_actor uuid, p_resolution_state_code text DEFAULT NULL::text, p_notes text DEFAULT NULL::text, p_meta jsonb DEFAULT NULL::jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_action_type_id int;
v_resolution_state_id int;
v_action_id uuid := gen_random_uuid();
v_snapshot_id uuid;
BEGIN
-- Validate cycle belongs to org + customer and is open
IF NOT EXISTS (
SELECT 1
FROM public.delinquency_cycles dc
WHERE dc.id = p_cycle_id
AND dc.organization_id = p_organization_id
AND dc.customer_id = p_customer_id
AND dc.is_deleted = false
) THEN
RAISE EXCEPTION 'Invalid cycle for org/customer';
END IF;
-- Resolve action type
SELECT dat.id INTO v_action_type_id
FROM public.delinquency_action_types dat
WHERE dat.code = p_action_type_code;
IF v_action_type_id IS NULL THEN
RAISE EXCEPTION 'Unknown action type code %', p_action_type_code;
END IF;
-- Resolve resolution state
IF p_resolution_state_code IS NOT NULL THEN
SELECT drs.id INTO v_resolution_state_id
FROM public.delinquency_resolution_states drs
WHERE drs.code = p_resolution_state_code;
END IF;
-- Attach current active snapshot (if any)
SELECT ds.id INTO v_snapshot_id
FROM public.delinquency_snapshots ds
WHERE ds.cycle_id = p_cycle_id
AND ds.is_deleted = false
ORDER BY ds.created_on_utc DESC
LIMIT 1;
-- Insert action
INSERT INTO public.delinquency_actions (
id, organization_id, customer_id, cycle_id, snapshot_id,
action_type_id, resolution_state_id,
notes, meta, action_on_utc,
created_by, created_on_utc, is_deleted
)
VALUES (
v_action_id, p_organization_id, p_customer_id, p_cycle_id, v_snapshot_id,
v_action_type_id, v_resolution_state_id,
p_notes, p_meta, p_action_on_utc,
p_actor, now(), false
);
-- Update cycle resolution state if provided
IF v_resolution_state_id IS NOT NULL THEN
UPDATE public.delinquency_cycles
SET resolution_state_id = v_resolution_state_id,
modified_on_utc = now(),
modified_by = p_actor
WHERE id = p_cycle_id;
END IF;
-- Upsert resolution window if TIME_GRANTED or PROMISE_TO_PAY
IF p_resolution_state_code IN ('TIME_GRANTED', 'PROMISE_TO_PAY') THEN
INSERT INTO public.delinquency_resolution_window (
cycle_id, pause_evaluation_until, reason,
created_on_utc, created_by, is_deleted
)
VALUES (
p_cycle_id,
COALESCE(
(p_meta ->> 'expectedDate')::date,
(p_meta ->> 'promiseDate')::date,
CURRENT_DATE
),
p_notes,
now(),
p_actor,
false
)
ON CONFLICT (cycle_id) DO UPDATE
SET pause_evaluation_until = EXCLUDED.pause_evaluation_until,
reason = COALESCE(EXCLUDED.reason, public.delinquency_resolution_window.reason),
is_deleted = false,
modified_by = p_actor,
modified_on_utc = now();
END IF;
-- If SETTLED or WAIVED -> close cycle + deactivate snapshot data
IF p_resolution_state_code IN ('SETTLED', 'WAIVED') THEN
UPDATE public.delinquency_cycles
SET ended_on_utc = now(),
status_id = 2,
modified_on_utc = now(),
modified_by = p_actor
WHERE id = p_cycle_id
AND ended_on_utc IS NULL;
-- Soft delete active snapshot
UPDATE public.delinquency_snapshots
SET is_deleted = true,
deleted_on_utc = now(),
deleted_by = p_actor
WHERE cycle_id = p_cycle_id
AND is_deleted = false;
-- Soft delete snapshot invoices
UPDATE public.delinquency_snapshot_invoices
SET is_deleted = true,
deleted_on_utc = now(),
deleted_by = p_actor
WHERE snapshot_id = v_snapshot_id
AND is_deleted = false;
-- Soft delete resolution window
UPDATE public.delinquency_resolution_window
SET is_deleted = true,
deleted_on_utc = now(),
deleted_by = p_actor
WHERE cycle_id = p_cycle_id
AND is_deleted = false;
END IF;
RETURN jsonb_build_object(
'action',
(SELECT row_to_json(a)
FROM (
SELECT da.id, da.organization_id, da.customer_id, da.cycle_id, da.snapshot_id,
dat.code AS action_type_code,
drs.code AS resolution_state_code,
da.notes, da.meta, da.action_on_utc
FROM public.delinquency_actions da
JOIN public.delinquency_action_types dat ON dat.id = da.action_type_id
LEFT JOIN public.delinquency_resolution_states drs ON drs.id = da.resolution_state_id
WHERE da.id = v_action_id
) a),
'cycle',
(SELECT row_to_json(c)
FROM (
SELECT dc.id, dc.resolution_state_id, dc.ended_on_utc, dc.status_id
FROM public.delinquency_cycles dc
WHERE dc.id = p_cycle_id
) c)
);
END;
$function$
-- Function: create_discussion_message
CREATE OR REPLACE FUNCTION public.create_discussion_message(p_organization_id uuid, p_cycle_id uuid, p_sender_id uuid, p_sender_type text, p_message_text text, p_sent_on_utc timestamp with time zone, p_actor uuid)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_message_id uuid := gen_random_uuid();
v_result jsonb;
BEGIN
-- Verify the cycle exists and belongs to the organization
IF NOT EXISTS(
SELECT 1
FROM public.delinquency_cycles dc
WHERE dc.id = p_cycle_id
AND dc.organization_id = p_organization_id
AND dc.is_deleted = false
) THEN
RAISE EXCEPTION 'Delinquency cycle % not found or does not belong to organization %', p_cycle_id, p_organization_id;
END IF;
-- Validate sender_type
IF p_sender_type NOT IN ('RESIDENT', 'FACILITY_MANAGER') THEN
RAISE EXCEPTION 'Invalid sender_type: %. Must be RESIDENT or FACILITY_MANAGER', p_sender_type;
END IF;
-- Insert the message
INSERT INTO public.delinquency_discussion_messages (
id,
organization_id,
cycle_id,
sender_id,
sender_type,
message_text,
sent_on_utc,
created_by,
created_on_utc,
is_deleted
)
VALUES (
v_message_id,
p_organization_id,
p_cycle_id,
p_sender_id,
p_sender_type,
p_message_text,
p_sent_on_utc,
p_actor,
now(),
false
);
-- Return the created message
v_result := jsonb_build_object(
'message',
(SELECT row_to_json(msg)
FROM (SELECT ddm.id,
ddm.cycle_id,
ddm.sender_id,
ddm.sender_type,
ddm.message_text,
ddm.sent_on_utc
FROM public.delinquency_discussion_messages ddm
WHERE ddm.id = v_message_id) msg)
);
RETURN v_result;
END;
$function$
-- Function: get_defaulter_contact
CREATE OR REPLACE FUNCTION public.get_defaulter_contact(p_customer_id uuid)
RETURNS text
LANGUAGE plpgsql
AS $function$
DECLARE
v_contact_number text;
BEGIN
SELECT
r.contact_number INTO v_contact_number
FROM
fdw_community.units u
INNER JOIN
fdw_community.resident_units ru ON u.id = ru.unit_id
INNER JOIN
fdw_community.residents r ON ru.resident_id = r.id
WHERE
u.customer_id = p_customer_id
AND u.is_deleted = false
AND ru.is_deleted = false
AND r.is_deleted = false
AND r.contact_number IS NOT NULL
LIMIT 1;
RETURN v_contact_number;
END;
$function$
-- Function: get_defaulter_list
CREATE OR REPLACE FUNCTION public.get_defaulter_list(p_company_id uuid)
RETURNS TABLE(id integer, cycle_id uuid, customer_id uuid, snapshot_id uuid, overdue_amount numeric, days_past_due integer, aging_bucket text, resolution_state text, is_anonymized boolean, can_reveal_unit boolean, next_expected_action_date date, customer_name text, resident_contact text, oldest_due_date date)
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id uuid;
BEGIN
/* ------------------------------------------------------------
1️⃣ Resolve organization_id
------------------------------------------------------------ */
SELECT c.organization_id
INTO v_organization_id
FROM public.companies c
WHERE c.id = p_company_id;
/* ------------------------------------------------------------
2️⃣ Main query
------------------------------------------------------------ */
RETURN QUERY
WITH policy AS (
SELECT
COALESCE(cp.grace_days, 0)::integer AS grace_days,
COALESCE(cp.anonymize_until_days, 0)::integer AS anonymize_until_days,
COALESCE(cp.reveal_unit_after_days, 0)::integer AS reveal_unit_after_days
FROM public.collection_policies cp
WHERE cp.organization_id = v_organization_id
AND cp.is_deleted = false
ORDER BY cp.created_on_utc DESC
LIMIT 1
),
open_cycles AS (
SELECT
dc.id,
dc.customer_id,
dc.resolution_state_id
FROM public.delinquency_cycles dc
WHERE dc.company_id = p_company_id
AND dc.ended_on_utc IS NULL
AND dc.is_deleted = false
),
active_snapshots AS (
SELECT
ds.id,
ds.cycle_id,
ds.overdue_amount,
ds.days_past_due,
ds.aging_bucket::text,
ds.oldest_due_date::date
FROM public.delinquency_snapshots ds
WHERE ds.company_id = p_company_id
AND ds.is_deleted = false
),
last_action AS (
SELECT DISTINCT ON (da.cycle_id)
da.cycle_id,
da.meta,
da.action_on_utc
FROM public.delinquency_actions da
WHERE da.company_id = p_company_id
AND da.is_deleted = false
ORDER BY da.cycle_id, da.action_on_utc DESC
),
base AS (
SELECT
/* row_number() → bigint → integer */
row_number() OVER (
ORDER BY ds.days_past_due DESC, ds.overdue_amount DESC
)::integer AS row_no,
oc.id AS cycle_id,
oc.customer_id,
ds.id AS snapshot_id,
ds.overdue_amount,
ds.days_past_due,
ds.aging_bucket,
rs.code::text AS resolution_state_code,
/* Policy-driven flags */
(ds.days_past_due < COALESCE(p.anonymize_until_days, 0)) AS is_anonymized,
(ds.days_past_due >= COALESCE(p.reveal_unit_after_days, 0)) AS can_reveal_unit,
/* JSON → text → date */
NULLIF(la.meta ->> 'expectedDate', '')::date
AS next_expected_action_date,
cust.name::text AS customer_name,
r.contact_number::text AS resident_contact,
ds.oldest_due_date
FROM open_cycles oc
JOIN active_snapshots ds
ON ds.cycle_id = oc.id
LEFT JOIN policy p ON TRUE
LEFT JOIN last_action la ON la.cycle_id = oc.id
LEFT JOIN public.delinquency_resolution_states rs
ON rs.id = oc.resolution_state_id
LEFT JOIN public.customers cust
ON cust.id = oc.customer_id
/* ---------- Community FDW joins ---------- */
LEFT JOIN fdw_community.units u
ON u.customer_id = oc.customer_id
AND u.is_deleted = false
/* Pick exactly ONE resident per unit */
LEFT JOIN LATERAL (
SELECT DISTINCT ON (ru.unit_id)
ru.unit_id,
ru.resident_id
FROM fdw_community.resident_units ru
JOIN fdw_community.residents r2
ON r2.id = ru.resident_id
AND r2.is_deleted = false
AND r2.contact_number IS NOT NULL
WHERE ru.unit_id = u.id
AND ru.is_deleted = false
ORDER BY
ru.unit_id,
ru.is_primary_owner DESC,
ru.created_on_utc ASC
) ru ON TRUE
LEFT JOIN fdw_community.residents r
ON r.id = ru.resident_id
/* Visibility rule */
WHERE ds.days_past_due >= COALESCE(p.reveal_unit_after_days, 0)
)
SELECT
b.row_no AS id,
b.cycle_id,
b.customer_id,
b.snapshot_id,
b.overdue_amount,
b.days_past_due,
b.aging_bucket,
COALESCE(b.resolution_state_code, 'NONE')::text AS resolution_state,
b.is_anonymized,
b.can_reveal_unit,
b.next_expected_action_date,
b.customer_name,
b.resident_contact,
b.oldest_due_date
FROM base b
ORDER BY
b.days_past_due DESC,
b.overdue_amount DESC;
END;
$function$
-- Function: get_defaulter_details
CREATE OR REPLACE FUNCTION public.get_defaulter_details(p_company_id uuid, p_customer_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_cycle_id uuid;
v_snapshot_id uuid;
BEGIN
SELECT dc.id
INTO v_cycle_id
FROM public.delinquency_cycles dc
WHERE dc.company_id = p_company_id
AND dc.customer_id = p_customer_id
AND dc.ended_on_utc IS NULL
AND dc.is_deleted = false
ORDER BY dc.started_on_utc DESC
LIMIT 1;
IF v_cycle_id IS NULL THEN
RETURN '{}'::jsonb;
END IF;
SELECT ds.id
INTO v_snapshot_id
FROM public.delinquency_snapshots ds
WHERE ds.cycle_id = v_cycle_id
AND ds.is_deleted = false
ORDER BY ds.created_on_utc DESC
LIMIT 1;
RETURN jsonb_build_object(
'cycle',
(SELECT row_to_json(c)
FROM (
SELECT dc.id, dc.organization_id, dc.company_id, dc.customer_id,
dc.start_due_date, dc.started_on_utc, dc.ended_on_utc,
dc.status_id, dc.resolution_state_id
FROM public.delinquency_cycles dc
WHERE dc.id = v_cycle_id
) c),
'snapshot',
(SELECT row_to_json(s)
FROM (
SELECT ds.id, ds.cycle_id, ds.company_id, ds.customer_id,
ds.overdue_amount, ds.oldest_due_date,
ds.days_past_due, ds.aging_bucket,
ds.last_evaluated_on
FROM public.delinquency_snapshots ds
WHERE ds.id = v_snapshot_id
) s),
'invoices',
(SELECT COALESCE(jsonb_agg(row_to_json(inv)), '[]'::jsonb)
FROM (
SELECT dsi.invoice_id, dsi.snapshot_id, dsi.company_id,
dsi.invoice_date, dsi.due_date,
dsi.total_amount, dsi.settled_amount, dsi.outstanding_amount,
ih.invoice_number, ih.note
FROM public.delinquency_snapshot_invoices dsi
JOIN public.invoice_headers ih ON ih.id = dsi.invoice_id
WHERE dsi.snapshot_id = v_snapshot_id
AND dsi.is_deleted = false
) inv),
'actions',
(SELECT COALESCE(jsonb_agg(row_to_json(act) ORDER BY act.action_on_utc DESC), '[]'::jsonb)
FROM (
SELECT da.id, da.cycle_id, da.customer_id, da.organization_id, da.snapshot_id,
dat.code AS action_type_code,
drs.code AS resolution_state_code,
da.notes, da.meta, da.action_on_utc
FROM public.delinquency_actions da
JOIN public.delinquency_action_types dat ON dat.id = da.action_type_id
LEFT JOIN public.delinquency_resolution_states drs ON drs.id = da.resolution_state_id
WHERE da.cycle_id = v_cycle_id
AND da.is_deleted = false
) act)
);
END;
$function$
-- Function: get_canonical_overdue_invoices
CREATE OR REPLACE FUNCTION public.get_canonical_overdue_invoices(p_company_id uuid)
RETURNS TABLE(company_id uuid, customer_id uuid, invoice_id uuid, invoice_date date, due_date date, total_amount numeric, settled_amount numeric, outstanding_amount numeric, days_past_due integer)
LANGUAGE sql
STABLE
AS $function$
SELECT
ih.company_id,
ih.customer_id,
ih.id AS invoice_id,
ih.invoice_date::date,
ih.due_date::date,
ih.total_amount,
COALESCE(ih.settled_amount, 0) AS settled_amount,
(ih.total_amount - COALESCE(ih.settled_amount, 0)) AS outstanding_amount,
(CURRENT_DATE - (ih.due_date::date + cp.grace_days))::int AS days_past_due
FROM public.invoice_headers ih
JOIN public.companies co
ON co.id = ih.company_id
AND co.is_deleted = false
JOIN public.collection_policies cp
ON cp.organization_id = co.organization_id
AND cp.is_deleted = false
WHERE ih.company_id = p_company_id
AND ih.is_deleted = false
AND (ih.total_amount - COALESCE(ih.settled_amount, 0)) > 0
AND (ih.due_date::date + cp.grace_days) < CURRENT_DATE;
$function$
-- Function: get_defaulter_summary
CREATE OR REPLACE FUNCTION public.get_defaulter_summary(p_company_id uuid)
RETURNS TABLE(id integer, total_defaulters_count integer, total_outstanding numeric)
LANGUAGE sql
STABLE
AS $function$
SELECT
1 AS id,
COUNT(DISTINCT customer_id)::int AS total_defaulters_count,
SUM(outstanding_amount) AS total_outstanding
FROM public.get_canonical_overdue_invoices(p_company_id);
$function$
-- Procedure: insert_invoice_by_excel
CREATE OR REPLACE PROCEDURE public.insert_invoice_by_excel(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_discount numeric, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_invoice_number text, IN p_payment_term integer, IN p_invoice_status_id integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_note text, IN p_round_off numeric, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_so_no text, IN p_so_date timestamp without time zone, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_invoice_voucher text, IN p_created_by uuid, IN p_invoice_details jsonb, IN p_type integer, IN p_settled_amount numeric DEFAULT 0)
LANGUAGE plpgsql
AS $procedure$
DECLARE
detail RECORD;
v_source_warehouse_id uuid;
v_destination_warehouse_id uuid;
v_invoice_number text;
BEGIN
-- Set default values for warehouses
v_source_warehouse_id := COALESCE(p_source_warehouse_id, NULL);
v_destination_warehouse_id := COALESCE(p_destination_warehouse_id, NULL);
IF p_invoice_number IS NULL OR p_invoice_number = '' THEN
v_invoice_number := get_new_invoice_number(p_company_id, p_invoice_date::date);
ELSE
v_invoice_number = p_invoice_number;
END IF;
-- Validate JSON structure
BEGIN
PERFORM jsonb_array_elements(p_invoice_details)::jsonb;
EXCEPTION
WHEN others THEN
RAISE EXCEPTION 'Invalid JSON format for invoice details';
END;
RAISE NOTICE 'Processing Invoice: %', p_id;
-- Insert into the invoice header
INSERT INTO
public.invoice_headers(
id,
company_id,
customer_id,
credit_account_id,
debit_account_id,
invoice_number,
invoice_date,
due_date,
payment_term,
taxable_amount,
cgst_amount,
sgst_amount,
igst_amount,
total_amount,
discount,
fees,
round_off,
currency_id,
note,
invoice_status_id,
created_by,
invoice_voucher,
source_warehouse_id,
destination_warehouse_id,
created_on_utc,
so_no,
so_date,
type
)
VALUES (
p_id,
p_company_id,
p_customer_id,
p_credit_account_id,
p_debit_account_id,
v_invoice_number,
p_invoice_date,
p_due_date,
p_payment_term,
p_taxable_amount,
p_cgst_amount,
p_sgst_amount,
p_igst_amount,
p_total_amount,
p_discount,
p_fees,
p_round_off,
p_currency_id,
p_note,
p_invoice_status_id,
p_created_by,
p_invoice_voucher,
p_source_warehouse_id,
p_destination_warehouse_id,
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
p_so_no,
p_so_date,
p_type
);
RAISE NOTICE 'Invoice header inserted with ID: %', p_id;
-- Loop through the JSONB array of invoice details
FOR detail IN
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
id uuid, product_id uuid, price numeric, quantity numeric,
fees numeric, discount numeric, taxable_amount numeric,
sgst_amount numeric, cgst_amount numeric, igst_amount numeric,
total_amount numeric, serial_number integer
)
LOOP
-- Insert into invoice details
INSERT INTO public.invoice_details (
id, invoice_header_id, product_id, price, quantity, fees, discount,
taxable_amount, sgst_amount, cgst_amount, igst_amount, total_amount,
serial_number, is_deleted
) VALUES (
detail.id, p_id, detail.product_id, detail.price, detail.quantity, detail.fees,
detail.discount, detail.taxable_amount, detail.sgst_amount, detail.cgst_amount,
detail.igst_amount, detail.total_amount, detail.serial_number, false
);
RAISE NOTICE 'Invoice detail inserted with ID: %', detail.id;
END LOOP;
-- No explicit COMMIT here
RAISE NOTICE 'Transaction completed successfully';
EXCEPTION
-- Catch all other exceptions
WHEN OTHERS THEN
RAISE EXCEPTION 'An error occurred: %', SQLERRM;
END;
$procedure$
-- Procedure: create_apartment_invoice_template
CREATE OR REPLACE PROCEDURE public.create_apartment_invoice_template(IN p_group_invoice_template_id uuid, IN p_calculation_type integer, IN p_company_id uuid, IN p_account_receivable_id uuid, IN p_sales_account_id uuid, IN p_invoice_date date, IN p_starts_from date, IN p_end_date date, IN p_payment_term integer, IN p_currency_id integer, IN p_fixed_product_id uuid, IN p_bhk_product_id uuid, IN p_sqft_product_id uuid, IN p_fixed_product_rate numeric, IN p_bhk_product_rate numeric, IN p_sqft_product_rate numeric, IN p_note character varying, IN p_invoice_status_id integer, IN p_due_days integer, IN p_billing_cycle text, IN p_created_by uuid, IN p_units json, IN p_active_status boolean, IN p_day_of_week integer DEFAULT NULL::integer, IN p_day_of_month integer DEFAULT NULL::integer, IN p_month_of_year integer DEFAULT NULL::integer, IN p_quarters_of_month integer DEFAULT NULL::integer, IN p_half_year integer DEFAULT NULL::integer, IN p_bi_month integer DEFAULT NULL::integer, IN p_time_of_day interval DEFAULT '00:00:00'::interval)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_unit json;
v_unit_id integer;
v_bhk numeric;
v_sqft_area numeric;
v_customer_id uuid;
v_total_amount numeric;
v_i integer := 0;
v_day_of_week integer := COALESCE(p_day_of_week, 0);
v_day_of_month integer := COALESCE(p_day_of_month, 0);
v_month_of_year integer := COALESCE(p_month_of_year, 0);
v_quarters_of_month integer := COALESCE(p_quarters_of_month, 0);
v_half_year integer := COALESCE(p_half_year, 0);
v_bi_month integer := COALESCE(p_bi_month, 0);
v_time_of_day interval := COALESCE(p_time_of_day, '00:00:00'::interval);
BEGIN
-- Insert into public.group_invoice_templates
INSERT INTO public.group_invoice_templates(
id, calculation_type_id, account_receivable_id, sales_account_id, invoice_date, starts_from, end_date,
grace_period, due_days, fixed_rate, bhk_rate, sqft_rate, invoice_description,
billing_cycle, created_by, created_on_utc, company_id, currency_id, bhk_product_id, fixed_product_id, sqft_product_id, active_status)
VALUES (
p_group_invoice_template_id, p_calculation_type, p_account_receivable_id, p_sales_account_id, p_invoice_date, p_starts_from, p_end_date,
p_payment_term, p_due_days, p_fixed_product_rate, p_bhk_product_rate, p_sqft_product_rate, p_note,
p_billing_cycle, p_created_by, NOW(), p_company_id, p_currency_id, p_bhk_product_id, p_fixed_product_id, p_sqft_product_id, p_active_status
);
-- Insert into batch_schedules based on billing cycle
IF p_billing_cycle = 'Daily' THEN
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, time_of_day, created_on_utc, created_by)
VALUES (
p_group_invoice_template_id, p_billing_cycle, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Weekly' THEN
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, day_of_week, time_of_day, created_on_utc, created_by)
VALUES (
p_group_invoice_template_id, p_billing_cycle, v_day_of_week, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Monthly' THEN
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_group_invoice_template_id, p_billing_cycle, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Bi-Monthly' THEN
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, bi_month, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_group_invoice_template_id, p_billing_cycle, v_bi_month, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Quarterly' THEN
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, quarters_of_month, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_group_invoice_template_id, p_billing_cycle, v_quarters_of_month, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Half-yearly' THEN
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, half_year, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_group_invoice_template_id, p_billing_cycle, v_half_year, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Yearly' THEN
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, month_of_year, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_group_invoice_template_id, p_billing_cycle, v_month_of_year, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSE
RAISE EXCEPTION 'Unknown billing cycle: %', p_billing_cycle;
END IF;
-- Loop through units JSON array and insert into apartment_invoice_template_units
FOR v_i IN 0 .. json_array_length(p_units) - 1 LOOP
v_unit := p_units->v_i;
v_unit_id := (v_unit->>'id')::integer;
v_bhk := (v_unit->>'bhk')::numeric;
v_sqft_area := (v_unit->>'sqftArea')::numeric;
v_customer_id := (v_unit->>'customerId')::uuid;
IF v_customer_id IS NULL THEN
RAISE EXCEPTION 'Customer ID is null for unit ID %', v_unit_id;
END IF;
INSERT INTO public.group_invoice_template_customers(
id, group_invoice_template_id, unit_id, bhk, sqft_area, customer_id, created_on_utc, created_by)
VALUES (
gen_random_uuid(), p_group_invoice_template_id, v_unit_id, v_bhk, v_sqft_area, v_customer_id, NOW(), p_created_by
);
END LOOP;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Error occurred: %', SQLERRM;
END;
$procedure$
-- Procedure: create_draft_invoice_detail
CREATE OR REPLACE PROCEDURE public.create_draft_invoice_detail(IN p_invoice_header_id uuid, IN p_price numeric, IN p_quantity numeric, IN p_discount numeric, IN p_fees numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_sgst_amount numeric, IN p_taxable_amount numeric, IN p_total_amount numeric, IN p_serial_number integer, IN p_product_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
p_id uuid;
BEGIN
p_id := gen_random_uuid();
INSERT INTO public.draft_invoice_details(
id,
invoice_header_id,
price,
quantity,
cgst_amount,
discount,
fees,
igst_amount,
sgst_amount,
taxable_amount,
total_amount,
serial_number,
product_id
)
VALUES (
p_id,
p_invoice_header_id,
p_price,
p_quantity,
p_cgst_amount,
p_discount,
p_fees,
p_igst_amount,
p_sgst_amount,
p_taxable_amount,
p_total_amount,
p_serial_number,
p_product_id
);
END;
$procedure$
-- Procedure: create_invoice_template
CREATE OR REPLACE PROCEDURE public.create_invoice_template(IN p_invoiceheader_id uuid, IN p_frequency_cycle text, IN p_month_of_year integer, IN p_day_of_month integer, IN p_day_of_week integer, IN p_time_of_day time without time zone, IN p_starts_from date, IN p_end_date date, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_template_id uuid := gen_random_uuid(); -- Generate a new UUID for the template ID
BEGIN
-- Insert the new template into the invoice_template table
INSERT INTO public.invoice_template (
id, invoice_header_id, frequency_cycle, month_of_year, day_of_month,
day_of_week, time_of_day, starts_from, end_date, created_on_utc, created_by, is_deleted
)
VALUES (
v_template_id, p_invoiceheader_id, p_frequency_cycle, p_month_of_year, p_day_of_month,
p_day_of_week, p_time_of_day, p_starts_from, p_end_date, NOW(), p_created_by, false
);
RAISE NOTICE 'Invoice template created successfully with ID: %', v_template_id;
END;
$procedure$
-- Procedure: create_multiple_invoice_payments
CREATE OR REPLACE PROCEDURE public.create_multiple_invoice_payments(IN p_invoice_payments jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
payment RECORD;
payment_statuses TEXT[] := ARRAY[]::TEXT[];
BEGIN
-- Loop through each payment in the JSONB array
FOR payment IN
SELECT * FROM jsonb_to_recordset(p_invoice_payments) AS (
invoice_payment_header_id uuid,
company_id uuid,
customer_id uuid,
received_date date,
mode_of_payment text,
credit_account_id uuid,
debit_account_id uuid,
reference text,
received_amount numeric,
grand_total_amount numeric,
advance_amount numeric,
description text,
tds_amount numeric,
created_by uuid,
invoice_payment_details jsonb
)
LOOP
BEGIN
IF NOT EXISTS (
SELECT 1
FROM public.invoice_payment_headers
WHERE reference = payment.reference
AND company_id = payment.company_id
) THEN
CALL public.create_invoice_payment(
payment.invoice_payment_header_id,
payment.company_id,
payment.customer_id,
payment.received_date,
payment.mode_of_payment,
payment.credit_account_id,
payment.debit_account_id,
payment.reference,
payment.received_amount,
payment.grand_total_amount,
payment.advance_amount,
payment.description,
payment.tds_amount,
payment.created_by,
payment.invoice_payment_details
);
payment_statuses := payment_statuses || format('%s:success', payment.invoice_payment_header_id);
ELSE
RAISE NOTICE 'Duplicate invoice payment found for reference: %, company_id: %. Skipping.',
payment.reference, payment.company_id;
payment_statuses := payment_statuses || format('%s:duplicate', payment.invoice_payment_header_id);
CONTINUE;
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Error processing payment ID: %, Error: %',
payment.invoice_payment_header_id, SQLERRM;
payment_statuses := payment_statuses || format('%s:failed', payment.invoice_payment_header_id);
END;
END LOOP;
-- Final status notice (for app to parse)
RAISE NOTICE 'PAYMENT_STATUS:%', to_json(payment_statuses);
RAISE NOTICE 'All invoice payments processed.';
END;
$procedure$
-- Procedure: delete_apartment_invoice_template
CREATE OR REPLACE PROCEDURE public.delete_apartment_invoice_template(IN p_template_id uuid, IN p_modified_by uuid, IN p_deleted_on_utc timestamp without time zone)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Begin transaction to ensure atomicity
PERFORM 1 FROM public.apartment_invoice_templates WHERE id = p_template_id;
IF FOUND THEN
-- Mark all related apartment_invoice_template_units as deleted
UPDATE public.apartment_invoice_template_units
SET
is_deleted = TRUE, -- Use TRUE for boolean
modified_by = p_modified_by,
deleted_on_utc = p_deleted_on_utc
WHERE
template_id = p_template_id;
-- Mark the template itself as deleted
UPDATE public.apartment_invoice_templates
SET
is_deleted = TRUE, -- Use TRUE for boolean
modified_by = p_modified_by,
deleted_on_utc = p_deleted_on_utc
WHERE
id = p_template_id;
ELSE
-- If the template does not exist, raise an exception
RAISE EXCEPTION 'InvoiceTemplate with template_id: % not found.', p_template_id;
END IF;
END;
$procedure$
-- Procedure: delete_invoice_data_by_company_id
CREATE OR REPLACE PROCEDURE public.delete_invoice_data_by_company_id(IN p_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Deleting from invoice_payment_details
DELETE FROM public.invoice_payment_details
USING public.invoice_payment_headers
WHERE invoice_payment_details.invoice_payment_header_id = invoice_payment_headers.id
AND invoice_payment_headers.company_id = p_company_id;
-- Deleting from invoice_payment_headers
DELETE FROM public.invoice_payment_headers
WHERE company_id = p_company_id;
-- Deleting from invoice_details
DELETE FROM public.invoice_details
USING public.invoice_headers
WHERE invoice_details.invoice_header_id = invoice_headers.id
AND invoice_headers.company_id = p_company_id;
-- Deleting from invoice_headers
DELETE FROM public.invoice_headers
WHERE company_id = p_company_id;
-- Deleting from draft_invoice_details
DELETE FROM public.draft_invoice_details
USING public.draft_invoice_headers
WHERE draft_invoice_details.invoice_header_id = draft_invoice_headers.id
AND draft_invoice_headers.company_id = p_company_id;
-- Deleting from draft_invoice_headers
DELETE FROM public.draft_invoice_headers
WHERE company_id = p_company_id;
END;
$procedure$
-- Procedure: initialize_invoice_header_ids
CREATE OR REPLACE PROCEDURE public.initialize_invoice_header_ids(IN p_default_company_id uuid, IN p_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_header_exists BOOLEAN;
v_max_id BIGINT;
v_last_seq_value BIGINT;
BEGIN
-- Check if invoice header IDs already exist for the new company
SELECT EXISTS (
SELECT 1
FROM invoice_header_ids
WHERE company_id = p_company_id
) INTO v_header_exists;
IF NOT v_header_exists THEN
-- Get the current maximum id from the invoice_header_ids table
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM invoice_header_ids;
-- Get the last value generated by the invoice_header_ids_id_seq sequence
SELECT last_value INTO v_last_seq_value FROM invoice_header_ids_id_seq;
-- Check if the sequence needs to be advanced
IF v_last_seq_value <= v_max_id THEN
-- Advance the sequence to be one greater than the maximum id
PERFORM setval('invoice_header_ids_id_seq', v_max_id + 1, false);
RAISE NOTICE 'Sequence invoice_header_ids_id_seq advanced to: %', v_max_id + 1;
END IF;
-- Insert new records for the new company
INSERT INTO invoice_header_ids (
id,
company_id,
fin_year,
invoice_prefix,
invoice_length,
last_invoice_id
)
SELECT
nextval('invoice_header_ids_id_seq'), -- Use a sequence for generating an integer ID
p_company_id, -- Assign the new company ID
fin_year, -- Copy financial year
invoice_prefix, -- Copy invoice prefix
invoice_length, -- Copy invoice length
0 -- Copy last invoice ID
FROM invoice_header_ids
WHERE company_id = p_default_company_id;
-- Optional: Log the operation
RAISE NOTICE 'Invoice header IDs initialized successfully for new company ID: %', p_company_id;
ELSE
RAISE NOTICE 'Invoice header IDs already exist for company %. Skipping initialization.', p_company_id;
END IF;
END;
$procedure$
-- Procedure: get_all_customer_names
CREATE OR REPLACE PROCEDURE public.get_all_customer_names(IN p_company_id uuid, OUT customer_list jsonb)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Fetch customer details (Id, Name) for the given CompanyId
SELECT jsonb_agg(
jsonb_build_object(
'Id', c.id,
'Name', c.name
)
)
INTO customer_list
FROM public.customers c
WHERE c.company_id = p_company_id;
-- If no customers are found, return an empty array
IF customer_list IS NULL THEN
customer_list := '[]'::jsonb;
END IF;
END;
$procedure$
-- Procedure: initialize_group_invoice_header_ids
CREATE OR REPLACE PROCEDURE public.initialize_group_invoice_header_ids(IN old_company_id uuid, IN new_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_header_exists BOOLEAN;
v_max_id BIGINT;
v_last_seq_value BIGINT;
BEGIN
-- Check if group invoice header IDs already exist for the new company
SELECT EXISTS (
SELECT 1
FROM group_invoice_header_ids
WHERE company_id = new_company_id
) INTO v_header_exists;
IF NOT v_header_exists THEN
-- Get the current maximum id from the group_invoice_header_ids table
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM group_invoice_header_ids;
-- Get the last value generated by the group_invoice_header_ids_id_seq sequence
SELECT last_value INTO v_last_seq_value FROM group_invoice_header_ids_id_seq;
-- Check if the sequence needs to be advanced
IF v_last_seq_value <= v_max_id THEN
-- Advance the sequence to be one greater than the maximum id
PERFORM setval('group_invoice_header_ids_id_seq', v_max_id + 1, false);
RAISE NOTICE 'Sequence group_invoice_header_ids_id_seq advanced to: %', v_max_id + 1;
END IF;
-- Insert new records for the new company
INSERT INTO group_invoice_header_ids (
id,
company_id,
fin_year,
group_invoice_prefix,
group_invoice_length,
last_group_invoice_id
)
SELECT
nextval('group_invoice_header_ids_id_seq'), -- Use a sequence for generating an integer ID
new_company_id, -- Assign the new company ID
fin_year, -- Copy the financial year
group_invoice_prefix, -- Copy the group invoice prefix
group_invoice_length, -- Copy the group invoice length
0 -- Copy the last group invoice ID need to change as default 0
FROM group_invoice_header_ids
WHERE company_id = old_company_id;
-- Optional: Log the operation
RAISE NOTICE 'Group invoice header IDs initialized successfully for new company ID: %', new_company_id;
ELSE
RAISE NOTICE 'Group invoice header IDs already exist for company %. Skipping initialization.', new_company_id;
END IF;
END;
$procedure$
-- Procedure: hard_delete_org_sales
CREATE OR REPLACE PROCEDURE public.hard_delete_org_sales(IN p_organization_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_company_id uuid;
BEGIN
FOR v_company_id IN SELECT id FROM companies WHERE organization_id = p_organization_id LOOP
DELETE FROM customer_note_headers WHERE company_id = v_company_id;
DELETE FROM customer_note_work_flows WHERE company_id = v_company_id;
DELETE FROM customers WHERE company_id = v_company_id;
DELETE FROM draft_invoice_header_ids WHERE company_id = v_company_id;
DELETE FROM draft_invoice_headers WHERE company_id = v_company_id;
DELETE FROM gate_pass_headers WHERE company_id = v_company_id;
DELETE FROM group_invoice_details WHERE company_id = v_company_id;
DELETE FROM group_invoice_header_ids WHERE company_id = v_company_id;
DELETE FROM group_invoice_headers WHERE company_id = v_company_id;
DELETE FROM group_invoice_templates WHERE company_id = v_company_id;
DELETE FROM invoice_header_ids WHERE company_id = v_company_id;
DELETE FROM invoice_headers WHERE company_id = v_company_id;
DELETE FROM invoice_payment_header_ids WHERE company_id = v_company_id;
DELETE FROM invoice_payment_headers WHERE company_id = v_company_id;
DELETE FROM invoice_voucher_ids WHERE company_id = v_company_id;
DELETE FROM invoice_workflow WHERE company_id = v_company_id;
DELETE FROM penalty_configs WHERE company_id = v_company_id;
DELETE FROM recurring_sales_schedules WHERE company_id = v_company_id;
DELETE FROM users WHERE company_id = v_company_id;
DELETE FROM companies WHERE id = v_company_id;
RAISE NOTICE 'Deleted sales data for company_id %', v_company_id;
END LOOP;
DELETE FROM organizations WHERE id = p_organization_id;
RAISE NOTICE 'Deleted sales data for organization_id %', p_organization_id;
END;
$procedure$
-- Procedure: hard_delete_organization
CREATE OR REPLACE PROCEDURE public.hard_delete_organization(IN p_org_id uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Delete records from dependent tables in reverse order of dependencies
-- Delete customer note workflows for the organization
DELETE FROM customer_note_work_flows
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_org_id
);
-- Delete invoice statuses for the organization
DELETE FROM invoice_statuses
WHERE created_by IN (
SELECT created_by FROM companies WHERE organization_id = p_org_id
);
-- Delete invoice workflows for the organization
DELETE FROM invoice_workflow
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_org_id
);
-- Delete draft invoice header IDs for the organization
DELETE FROM draft_invoice_header_ids
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_org_id
);
-- Delete invoice header IDs for the organization
DELETE FROM invoice_header_ids
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_org_id
);
-- Delete invoice payment header IDs for the organization
DELETE FROM invoice_payment_header_ids
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_org_id
);
-- Delete group invoice header IDs for the organization
DELETE FROM group_invoice_header_ids
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_org_id
);
-- Delete users associated with the organization
DELETE FROM users
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_org_id
);
-- Delete companies associated with the organization
DELETE FROM companies
WHERE organization_id = p_org_id;
-- Finally, delete the organization itself
DELETE FROM organizations
WHERE id = p_org_id;
-- Optional: Log the operation
RAISE NOTICE 'Organization and associated records deleted successfully for organization ID: %', p_org_id;
END;
$procedure$
-- Procedure: initialize_company
CREATE OR REPLACE PROCEDURE public.initialize_company(IN p_company_id uuid, IN p_org_id uuid, IN p_company_name text, IN p_is_apartment boolean, IN p_created_by uuid, IN p_default_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_company_exists boolean;
BEGIN
-- Check if organization already exists by ID
SELECT EXISTS (
SELECT 1 FROM public.companies WHERE id = p_company_id
) INTO v_company_exists;
IF v_company_exists THEN
RAISE NOTICE 'Company with ID % already exists. Skipping initialization.', p_company_id;
ELSE
-- Insert into companies table
INSERT INTO public.companies (
id,
organization_id,
name,
is_apartment,
created_on_utc,
created_by
) VALUES (
p_company_id, -- New Company ID
p_org_id, -- Organization ID
p_company_name, -- Organization Name
p_is_apartment, -- Is Apartment
NOW(), -- Current UTC timestamp
p_created_by -- Created by user
);
RAISE NOTICE 'Company Created successfully for company ID: %', p_company_id;
END IF;
-- Call procedures to initialize sales-related configurations
CALL public.initialize_invoice_workflow(
p_default_company_id, -- Old Company ID
p_company_id, -- New Company ID
p_created_by -- Created By User ID
);
CALL public.initialize_draft_invoice_header_ids(
p_default_company_id, -- Old Company ID
p_company_id -- New Company ID
);
CALL public.initialize_invoice_header_ids(
p_default_company_id, -- Old Company ID
p_company_id -- New Company ID
);
CALL public.initialize_invoice_payment_header_ids(
p_default_company_id, -- Old Company ID
p_company_id -- New Company ID
);
-- Add call to initialize group invoice header IDs
CALL public.initialize_group_invoice_header_ids(
p_default_company_id, -- Old Company ID
p_company_id -- New Company ID
);
-- Add call to initialize customer note workflows
CALL public.initialize_customer_note_work_flows(
p_default_company_id, -- Old Company ID
p_company_id, -- New Company ID
p_created_by -- Created By User ID
);
-- Optional: Log the operation
RAISE NOTICE 'Company initialization completed successfully for company ID: %', p_company_id;
END;
$procedure$
-- Procedure: initialize_customer_note_work_flows
CREATE OR REPLACE PROCEDURE public.initialize_customer_note_work_flows(IN p_old_company_id uuid, IN p_new_company_id uuid, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_workflow_exists BOOLEAN;
v_max_id BIGINT;
v_last_seq_value BIGINT;
BEGIN
-- Check if customer note workflows already exist for the new company
SELECT EXISTS (
SELECT 1
FROM customer_note_work_flows
WHERE company_id = p_new_company_id
) INTO v_workflow_exists;
IF NOT v_workflow_exists THEN
-- Get the current maximum id from the customer_note_work_flows table
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM customer_note_work_flows;
-- Get the last value generated by the customer_note_work_flows_id_seq sequence
SELECT last_value INTO v_last_seq_value FROM customer_note_work_flows_id_seq;
-- Check if the sequence needs to be advanced
IF v_last_seq_value <= v_max_id THEN
-- Advance the sequence to be one greater than the maximum id
PERFORM setval('customer_note_work_flows_id_seq', v_max_id + 1, false);
RAISE NOTICE 'Sequence customer_note_work_flows_id_seq advanced to: %', v_max_id + 1;
END IF;
-- Insert new records into customer_note_work_flows for the new company
INSERT INTO customer_note_work_flows (
id,
company_id,
status,
next_status,
previous_status,
is_initial,
created_on_utc,
created_by,
is_deleted
)
SELECT
nextval('customer_note_work_flows_id_seq'), -- Generate new IDs using the sequence
p_new_company_id, -- Assign the new company ID
status, -- Copy the status
next_status, -- Copy the next_status
previous_status, -- Copy the previous_status
is_initial, -- Copy the is_initial flag
NOW(), -- Set current UTC timestamp for created_on_utc
p_created_by, -- Assign the created_by user
false -- Set is_deleted to false for new records
FROM customer_note_work_flows
WHERE company_id = p_old_company_id -- Filter records by the old company ID
AND is_deleted = false; -- Only copy non-deleted records
-- Optional: Log the operation
RAISE NOTICE 'Customer note workflows have been copied from company % to company % by user %.', p_old_company_id, p_new_company_id, p_created_by;
ELSE
RAISE NOTICE 'Customer note workflows already exist for company %. Skipping initialization.', p_new_company_id;
END IF;
END;
$procedure$
-- Procedure: initialize_invoice_workflow
CREATE OR REPLACE PROCEDURE public.initialize_invoice_workflow(IN p_default_company_id uuid, IN p_company_id uuid, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_header_exists BOOLEAN;
v_max_id BIGINT;
v_last_seq_value BIGINT;
BEGIN
-- Check if invoice workflow already exists for the new company
SELECT EXISTS (
SELECT 1
FROM invoice_workflow
WHERE company_id = p_company_id
) INTO v_header_exists;
IF NOT v_header_exists THEN
-- Get the current maximum id from the invoice_workflow table
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM public.invoice_workflow;
-- Get the last value generated by the invoice_workflow_id_seq sequence
SELECT last_value INTO v_last_seq_value FROM invoice_workflow_id_seq;
-- Check if the sequence needs to be advanced
IF v_last_seq_value <= v_max_id THEN
-- Advance the sequence to be one greater than the maximum id
PERFORM setval('invoice_workflow_id_seq', v_max_id + 1, false);
RAISE NOTICE 'Sequence invoice_workflow_id_seq advanced to: %', v_max_id + 1;
END IF;
-- Insert new records for the new company
INSERT INTO invoice_workflow (
id,
company_id,
status,
next_status,
previous_status,
is_initial,
created_on_utc,
created_by,
approval_level
)
SELECT
nextval('invoice_workflow_id_seq'),
p_company_id,
status,
next_status,
previous_status,
is_initial,
NOW(),
p_created_by,
approval_level -- The approval_level from the default company
FROM invoice_workflow
WHERE company_id = p_default_company_id;
-- Optional: Log the operation
RAISE NOTICE 'Invoice workflow records have been copied from company % to company % by user %.', p_default_company_id, p_company_id, p_created_by;
ELSE
RAISE NOTICE 'Invoice workflow records already exist for company %. Skipping initialization.', p_company_id;
END IF;
END;
$procedure$
-- Procedure: log_invoice_approval
CREATE OR REPLACE PROCEDURE public.log_invoice_approval(IN p_invoice_id uuid, IN p_next_status integer, IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
APPROVED_STATUS CONSTANT INTEGER := 3;
FIRST_LEVEL_APPROVAL CONSTANT INTEGER := 8;
SECOND_LEVEL_APPROVAL CONSTANT INTEGER := 9;
BEGIN
IF p_next_status = APPROVED_STATUS OR
p_next_status = FIRST_LEVEL_APPROVAL OR
p_next_status = SECOND_LEVEL_APPROVAL THEN
INSERT INTO public.invoice_approval_logs(
invoice_id,
status_id,
approved_by,
approved_on,
"comment",
created_on_utc,
created_by
)
VALUES(
p_invoice_id,
p_next_status,
p_modified_by,
NOW(),
CASE
WHEN p_next_status = FIRST_LEVEL_APPROVAL THEN 'Level 1 Approval'
WHEN p_next_status = SECOND_LEVEL_APPROVAL THEN 'Level 2 Approval'
WHEN p_next_status = APPROVED_STATUS THEN 'Final Approval'
ELSE 'Status updated'
END,
NOW(),
p_modified_by
);
RAISE NOTICE 'Approval logged for invoice %, status %', p_invoice_id, p_next_status;
END IF;
END;
$procedure$
-- Procedure: run_batches
CREATE OR REPLACE PROCEDURE public.run_batches()
LANGUAGE plpgsql
AS $procedure$
DECLARE
schedule_record RECORD;
v_unit RECORD;
v_group_invoice_id UUID;
v_invoice_header_id UUID;
v_total_amount NUMERIC;
v_status TEXT;
v_error_message TEXT;
v_total_count INTEGER;
v_success_count INTEGER;
v_calculation_type INTEGER;
v_company_id UUID;
v_sales_account_id UUID;
v_account_receivable_id UUID;
v_invoice_date DATE;
v_starts_from DATE;
v_end_date DATE;
v_due_date DATE;
v_payment_term INTEGER;
v_currency_id INTEGER;
v_fixed_product_id UUID;
v_bhk_product_id UUID;
v_sqft_product_id UUID;
v_fixed_product_rate NUMERIC;
v_bhk_product_rate NUMERIC;
v_sqft_product_rate NUMERIC;
v_note TEXT;
v_invoice_status_id INTEGER;
v_due_days INTEGER;
v_billing_cycle TEXT;
v_active_status boolean;
-- Variables taken from batch_schedules table for the particular group_invoice_template_id
v_half_year INTEGER;
v_quarters_of_month INTEGER;
v_bi_month INTEGER;
v_day_of_week INTEGER; -- From batch_schedules table
v_day_of_month INTEGER; -- From batch_schedules table
v_month_of_year INTEGER; -- From batch_schedules table
v_time_of_day INTERVAL := '15:23:00'; -- Set time_of_day to 02:05 AM
v_created_by UUID;
v_group_invoice_number character varying;
--v_fin_year INTEGER;
BEGIN
-- Loop through all non-deleted schedules that should run today
FOR schedule_record IN
SELECT *
FROM batch_schedules
WHERE is_deleted = FALSE
AND group_invoice_template_id IN (
SELECT id
FROM public.group_invoice_templates
WHERE is_deleted = FALSE
AND (
-- Only process templates where start date <= current date and end date is either null or in the future
starts_from <= CURRENT_DATE
AND (end_date IS NULL OR end_date >= CURRENT_DATE)
)
)
-- Check if the time of day is 02:05 AM as per the requirement
AND last_executed_at IS DISTINCT FROM CURRENT_DATE::timestamp
LOOP
-- Now you can access end_date as it has been included in the query
v_half_year := schedule_record.half_year;
v_quarters_of_month := schedule_record.quarters_of_month;
v_bi_month := schedule_record.bi_month;
v_day_of_week := schedule_record.day_of_week;
v_day_of_month := schedule_record.day_of_month;
v_month_of_year := schedule_record.month_of_year;
-- Process based on billing cycle and check if current time matches the set time
IF (
-- Daily schedules
schedule_record.billing_cycle = 'Daily'
-- Weekly schedules running on the current day of the week
OR (schedule_record.billing_cycle = 'Weekly' AND v_day_of_week = EXTRACT(DOW FROM CURRENT_DATE))
-- Monthly schedules running on the current day of the month
OR (schedule_record.billing_cycle = 'Monthly' AND v_day_of_month = EXTRACT(DAY FROM CURRENT_DATE) AND CURRENT_DATE <= schedule_record.end_date)
-- Bi-Monthly schedules: run every two months
OR (schedule_record.billing_cycle = 'Bi-Monthly' AND v_bi_month = CEIL(EXTRACT(MONTH FROM CURRENT_DATE) / 2) AND CURRENT_DATE <= schedule_record.end_date)
-- Quarterly schedules: run every three months
OR (schedule_record.billing_cycle = 'Quarterly' AND v_quarters_of_month = CEIL(EXTRACT(MONTH FROM CURRENT_DATE) / 3) AND CURRENT_DATE <= schedule_record.end_date)
-- Half-yearly schedules: run every six months
OR (schedule_record.billing_cycle = 'Half-yearly' AND v_half_year = CASE WHEN EXTRACT(MONTH FROM CURRENT_DATE) <= 6 THEN 1 ELSE 2 END AND CURRENT_DATE <= schedule_record.end_date)
-- Yearly schedules: run based on month of the year
OR (schedule_record.billing_cycle = 'Yearly' AND v_month_of_year = EXTRACT(MONTH FROM CURRENT_DATE) AND CURRENT_DATE <= schedule_record.end_date)
)
-- Check if the time of day is 02:05 AM or later
AND CURRENT_TIME >= v_time_of_day THEN
-- Begin processing the batch for this template
v_group_invoice_id := gen_random_uuid();
v_total_count := 0;
v_success_count := 0;
v_status := 'pending';
v_error_message := '';
-- Get template details
SELECT calculation_type_id, company_id, sales_account_id, invoice_date, starts_from, grace_period, due_days,
fixed_rate, bhk_rate, sqft_rate, invoice_description, billing_cycle, account_receivable_id,
currency_id, bhk_product_id, fixed_product_id, sqft_product_id, end_date, created_by, active_status
INTO v_calculation_type, v_company_id, v_sales_account_id, v_invoice_date, v_starts_from, v_payment_term,
v_due_days, v_fixed_product_rate, v_bhk_product_rate, v_sqft_product_rate, v_note, v_billing_cycle,
v_account_receivable_id, v_currency_id, v_bhk_product_id, v_fixed_product_id, v_sqft_product_id, v_end_date,
v_created_by, v_active_status
FROM public.group_invoice_templates
WHERE id = schedule_record.group_invoice_template_id;
-- Skip processing if the template is not active
IF NOT v_active_status THEN
RAISE NOTICE 'Skipping template with ID: %, as active_status is FALSE', schedule_record.group_invoice_template_id;
CONTINUE; -- Move to the next schedule_record
END IF;
-- Calculate the due date
v_due_date := v_invoice_date + v_due_days;
-- Skip processing if the due date is before the starts_from date
IF v_starts_from IS NOT NULL AND v_due_date < v_starts_from THEN
CONTINUE;
END IF;
-- Skip processing if the template has ended
IF v_end_date IS NOT NULL AND v_end_date < CURRENT_DATE THEN
CONTINUE;
END IF;
v_group_invoice_number := get_new_group_invoice_number(v_company_id);--, v_fin_year);
RAISE NOTICE 'Value: %', v_group_invoice_number;
-- Insert into group_inovice_headers
INSERT INTO public.group_inovice_headers (
id, group_invoice_template_id, execution_date, success_count, created_on_utc, created_by, total_count, error_message, company_id, group_invoice_number
)
VALUES (
v_group_invoice_id, schedule_record.group_invoice_template_id, NOW(), v_success_count, NOW(), 'SYSTEM', v_total_count, v_error_message, v_company_id, v_group_invoice_number
);
-- Select Status from the invoice workflow
SELECT MIN(status)
INTO v_invoice_status_id
FROM public.invoice_workflow
WHERE company_id = v_company_id
AND is_deleted = FALSE;
-- Loop through units associated with the template
FOR v_unit IN
SELECT unit_id, bhk, sqft_area, customer_id
FROM public.group_invoice_template_customers
WHERE group_invoice_template_id = schedule_record.group_invoice_template_id
LOOP
BEGIN
v_total_count := v_total_count + 1;
v_invoice_header_id := gen_random_uuid();
v_total_amount := 0;
-- Calculate the total amount based on the calculation type
CASE v_calculation_type
WHEN 1 THEN -- Fixed method
v_total_amount := v_fixed_product_rate;
WHEN 2 THEN -- BHK
v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
WHEN 3 THEN -- SFT
v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
WHEN 4 THEN -- Fixed + BHK
v_total_amount := v_fixed_product_rate + (v_bhk_product_rate * (v_unit.bhk)::NUMERIC);
WHEN 5 THEN -- Fixed + SFT
v_total_amount := v_fixed_product_rate + (v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC);
END CASE;
IF v_calculation_type = 1 THEN --FIxed
CALL public.create_invoice_detail(v_invoice_header_id, v_fixed_product_rate, 1, 0, 0, 0, 0, 0, v_fixed_product_rate, v_fixed_product_rate, 1, v_fixed_product_id);
ELSIF v_calculation_type = 2 THEN -- BHK
v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
CALL public.create_invoice_detail(v_invoice_header_id, v_bhk_product_rate, (v_unit.bhk)::NUMERIC, 0, 0, 0, 0, 0, v_total_amount, v_total_amount, 1, v_bhk_product_id);
ELSIF v_calculation_type = 3 THEN -- SFT
v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
CALL public.create_invoice_detail(v_invoice_header_id, v_sqft_product_rate, (v_unit.sqft_area)::NUMERIC, 0, 0, 0, 0, 0, v_total_amount, v_total_amount, 1, v_sqft_product_id);
ELSIF v_calculation_type = 4 THEN -- Fixed + BHK
CALL public.create_invoice_detail(v_invoice_header_id, v_fixed_product_rate, 1, 0, 0, 0, 0, 0, v_fixed_product_rate, v_fixed_product_rate, 1, v_fixed_product_id);
v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
CALL public.create_invoice_detail(v_invoice_header_id, v_bhk_product_rate, (v_unit.bhk)::NUMERIC, 0, 0, 0, 0, 0, v_total_amount, v_total_amount, 2, v_bhk_product_id);
ELSIF v_calculation_type = 5 THEN -- Fixed + SFT
CALL public.create_invoice_detail(v_invoice_header_id, v_fixed_product_rate, 1, 0, 0, 0, 0, 0, v_fixed_product_rate, v_fixed_product_rate, 1, v_fixed_product_id);
v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
CALL public.create_invoice_detail(v_invoice_header_id, v_sqft_product_rate, (v_unit.sqft_area)::NUMERIC, 0, 0, 0, 0, 0, v_total_amount, v_total_amount, 2, v_sqft_product_id);
END IF;
v_status := 'success';
v_success_count := v_success_count + 1;
-- Insert into group_inovice_details
INSERT INTO public.group_inovice_details(
id, group_invoice_id, unit_id, invoice_header_id, status, posted_on, error_message, created_on_utc, created_by)
VALUES (
gen_random_uuid(), v_group_invoice_id, v_unit.unit_id, v_invoice_header_id, v_status, NOW(),v_error_message, NOW(), 'SYSTEM'
);
EXCEPTION
WHEN OTHERS THEN
-- Handle any errors during invoice posting
v_status := 'failed';
v_error_message := COALESCE(SQLERRM, 'Unknown error');
-- Insert into group_inovice_details with failed status
INSERT INTO public.group_inovice_details(
id, group_invoice_id, unit_id, status, invoice_header_id, error_message, posted_on, created_on_utc, created_by)
VALUES (
gen_random_uuid(), v_group_invoice_id, v_unit.unit_id, v_status, v_invoice_header_id, COALESCE(v_error_message, 'Unknown error'), NOW(), NOW(), 'SYSTEM'
);
END;
END LOOP;
-- Update execution status based on success/failure
IF v_success_count = v_total_count THEN
v_status := 'success';
ELSIF v_success_count > 0 THEN
v_status := 'partially failed';
ELSE
v_status := 'failed';
END IF;
-- Update execution record with final status and counts
UPDATE public.group_inovice_headers
SET success_count = v_success_count,
total_count = v_total_count,
modified_on_utc = NOW(),
modified_by = v_created_by,
error_message = COALESCE(v_error_message, '')
WHERE id = v_group_invoice_id;
-- Update last executed timestamp for the schedule
UPDATE batch_schedules
SET last_executed_at = NOW()
WHERE id = schedule_record.id;
END IF;
END LOOP;
END;
$procedure$
-- Procedure: run_grouped_invoices
CREATE OR REPLACE PROCEDURE public.run_grouped_invoices(IN p_template_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
schedule_record RECORD;
v_unit RECORD;
v_group_invoice_header_id UUID;
v_invoice_header_id UUID;
v_total_amount NUMERIC;
v_status TEXT;
v_error_message TEXT;
v_total_count INTEGER;
v_success_count INTEGER;
v_calculation_type INTEGER;
v_company_id UUID;
v_sales_account_id UUID;
v_account_receivable_id UUID;
v_invoice_date DATE;
v_starts_from DATE;
v_end_date DATE;
v_due_date DATE;
v_payment_term INTEGER;
v_currency_id INTEGER;
v_fixed_product_id UUID;
v_bhk_product_id UUID;
v_sqft_product_id UUID;
v_fixed_product_rate NUMERIC;
v_bhk_product_rate NUMERIC;
v_sqft_product_rate NUMERIC;
v_note TEXT;
v_invoice_status_id INTEGER;
v_due_days INTEGER;
v_billing_cycle TEXT;
v_active_status BOOLEAN;
v_day_of_week INTEGER;
v_day_of_month INTEGER;
v_month_of_year INTEGER;
v_time_of_day TIME;
v_created_by UUID;
v_group_invoice_number VARCHAR;
v_serial_number INTEGER := 1;
v_product_rate NUMERIC;
v_quantity NUMERIC;
v_product_id UUID;
v_template_exists BOOL;
v_template_is_deleted BOOL;
BEGIN
RAISE NOTICE '[START] run_grouped_invoices(template_id=%)', p_template_id;
-- Extra visibility on template state
SELECT EXISTS (SELECT 1 FROM public.group_invoice_templates WHERE id = p_template_id) AS exists,
COALESCE( (SELECT is_deleted FROM public.group_invoice_templates WHERE id = p_template_id LIMIT 1), NULL) AS is_deleted
INTO v_template_exists, v_template_is_deleted;
RAISE NOTICE '[CHECK] template exists? %, is_deleted? %', v_template_exists, v_template_is_deleted;
IF NOT EXISTS (
SELECT 1 FROM public.group_invoice_templates
WHERE id = p_template_id AND is_deleted = FALSE
) THEN
RAISE EXCEPTION 'Invalid template_id: % (exists=%, is_deleted=%)', p_template_id, v_template_exists, v_template_is_deleted;
END IF;
v_time_of_day := CURRENT_TIME;
RAISE NOTICE '[INFO] current_time=%', v_time_of_day;
FOR schedule_record IN
SELECT *
FROM batch_schedules bs
WHERE bs.group_invoice_template_id = p_template_id
AND bs.is_deleted = FALSE
AND bs.last_executed_at IS DISTINCT FROM CURRENT_DATE::timestamp
LOOP
RAISE NOTICE '[SCHEDULE] processing schedule_id=% last_executed_at=%', schedule_record.id, schedule_record.last_executed_at;
SELECT calculation_type_id, company_id, sales_account_id, invoice_date, starts_from, grace_period, due_days,
fixed_rate, bhk_rate, sqft_rate, invoice_description, billing_cycle, account_receivable_id,
currency_id, bhk_product_id, fixed_product_id, sqft_product_id, end_date, created_by, active_status
INTO v_calculation_type, v_company_id, v_sales_account_id, v_invoice_date, v_starts_from, v_payment_term,
v_due_days, v_fixed_product_rate, v_bhk_product_rate, v_sqft_product_rate, v_note, v_billing_cycle,
v_account_receivable_id, v_currency_id, v_bhk_product_id, v_fixed_product_id, v_sqft_product_id, v_end_date,
v_created_by, v_active_status
FROM public.group_invoice_templates git
WHERE git.id = p_template_id;
RAISE NOTICE '[TEMPLATE] company_id=% calc_type=% inv_date=% starts_from=% due_days=% end_date=% active=%',
v_company_id, v_calculation_type, v_invoice_date, v_starts_from, v_due_days, v_end_date, v_active_status;
-- Force System user
SELECT id INTO v_created_by
FROM users
WHERE first_name = 'System'
LIMIT 1;
RAISE NOTICE '[USER] created_by (System) id=%', v_created_by;
IF v_created_by IS NULL THEN
RAISE EXCEPTION 'System user not found in the users table';
END IF;
IF NOT v_active_status THEN
RAISE NOTICE '[SKIP] template inactive';
CONTINUE;
END IF;
v_due_date := v_invoice_date + v_due_days;
RAISE NOTICE '[DATES] invoice_date=% due_days=% due_date=%', v_invoice_date, v_due_days, v_due_date;
IF v_starts_from IS NOT NULL AND v_due_date < v_starts_from THEN
RAISE NOTICE '[SKIP] due_date % < starts_from %', v_due_date, v_starts_from;
CONTINUE;
END IF;
IF v_end_date IS NOT NULL AND v_end_date < CURRENT_DATE THEN
RAISE NOTICE '[SKIP] end_date % < today %', v_end_date, CURRENT_DATE;
CONTINUE;
END IF;
v_group_invoice_header_id := gen_random_uuid();
v_total_count := 0;
v_success_count := 0;
v_status := 'pending';
v_error_message := '';
v_group_invoice_number := get_new_group_invoice_number(v_company_id, v_invoice_date);
RAISE NOTICE '[GROUP] new group_invoice_header_id=% number=%', v_group_invoice_header_id, v_group_invoice_number;
INSERT INTO public.group_invoice_headers (
id, group_invoice_template_id, execution_date, success_count,
created_on_utc, created_by, total_count, error_message, company_id, group_invoice_number
)
VALUES (
v_group_invoice_header_id, p_template_id, NOW(), v_success_count,
NOW(), v_created_by, v_total_count, v_error_message, v_company_id, v_group_invoice_number
);
SELECT status
INTO v_invoice_status_id
FROM public.invoice_workflow iw
WHERE iw.company_id = v_company_id
AND iw.is_deleted = FALSE
AND iw.status = 3
LIMIT 1;
RAISE NOTICE '[WORKFLOW] invoice_status_id=%', v_invoice_status_id;
FOR v_unit IN
SELECT unit_id, bhk, sqft_area, customer_id
FROM public.group_invoice_template_customers gitc
WHERE gitc.group_invoice_template_id = p_template_id
LOOP
BEGIN
v_total_count := v_total_count + 1;
v_invoice_header_id := gen_random_uuid();
v_total_amount := 0;
v_serial_number := 1;
RAISE NOTICE ' [UNIT] unit_id=% customer_id=% bhk=% sqft=%', v_unit.unit_id, v_unit.customer_id, v_unit.bhk, v_unit.sqft_area;
-- Compute total first
CASE v_calculation_type
WHEN 1 THEN v_total_amount := v_fixed_product_rate;
WHEN 2 THEN v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
WHEN 3 THEN v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
WHEN 4 THEN v_total_amount := v_fixed_product_rate + (v_bhk_product_rate * (v_unit.bhk)::NUMERIC);
WHEN 5 THEN v_total_amount := v_fixed_product_rate + (v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC);
ELSE RAISE EXCEPTION 'Invalid calculation type: %', v_calculation_type;
END CASE;
RAISE NOTICE ' [AMOUNT] calc_type=% total_amount=%', v_calculation_type, v_total_amount;
-- HEADER first (fixes FK)
CALL public.create_invoice_header(
v_invoice_header_id,
v_company_id,
v_unit.customer_id,
v_account_receivable_id,
v_sales_account_id,
v_invoice_date,
v_due_date,
v_payment_term,
v_total_amount,
0,0,0,
v_total_amount,
0,0,0,
v_currency_id,
v_note,
v_invoice_status_id,
v_created_by,
'IVAP000',
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000000',
'AP',
v_invoice_date,
3
);
RAISE NOTICE ' [HEADER CREATED] invoice_header_id=%', v_invoice_header_id;
-- DETAILS now
CASE v_calculation_type
WHEN 1 THEN
v_product_rate := v_fixed_product_rate; v_quantity := 1; v_product_id := v_fixed_product_id;
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
RAISE NOTICE ' [DETAIL ADDED] fixed rate line';
WHEN 2 THEN
v_product_rate := v_bhk_product_rate * (v_unit.bhk)::NUMERIC; v_quantity := 1; v_product_id := v_bhk_product_id;
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
RAISE NOTICE ' [DETAIL ADDED] bhk line';
WHEN 3 THEN
v_product_rate := v_sqft_product_rate; v_quantity := v_unit.sqft_area; v_product_id := v_sqft_product_id;
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate*v_quantity, v_product_rate*v_quantity, v_serial_number, v_product_id);
RAISE NOTICE ' [DETAIL ADDED] sqft line';
WHEN 4 THEN
-- fixed
v_product_rate := v_fixed_product_rate; v_quantity := 1; v_product_id := v_fixed_product_id;
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
v_serial_number := v_serial_number + 1;
-- bhk
v_product_rate := v_bhk_product_rate * (v_unit.bhk)::NUMERIC; v_quantity := 1; v_product_id := v_bhk_product_id;
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
RAISE NOTICE ' [DETAIL ADDED] fixed + bhk lines';
WHEN 5 THEN
-- fixed
v_product_rate := v_fixed_product_rate; v_quantity := 1; v_product_id := v_fixed_product_id;
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate, v_product_rate, v_serial_number, v_product_id);
v_serial_number := v_serial_number + 1;
-- sqft
v_product_rate := v_sqft_product_rate; v_quantity := v_unit.sqft_area; v_product_id := v_sqft_product_id;
CALL public.create_invoice_detail(v_invoice_header_id, v_product_rate, v_quantity, 0,0, 0,0,0, v_product_rate*v_quantity, v_product_rate*v_quantity, v_serial_number, v_product_id);
RAISE NOTICE ' [DETAIL ADDED] fixed + sqft lines';
END CASE;
v_status := 'success';
v_success_count := v_success_count + 1;
INSERT INTO public.group_invoice_details(
id, group_invoice_header_id, unit_id, invoice_header_id, status, posted_on,
error_message, created_on_utc, created_by, company_id
)
VALUES (
gen_random_uuid(), v_group_invoice_header_id, v_unit.unit_id, v_invoice_header_id, v_status, NOW(),
'', NOW(), v_created_by, v_company_id
);
RAISE NOTICE ' [UNIT DONE] unit_id=% status=%', v_unit.unit_id, v_status;
EXCEPTION
WHEN OTHERS THEN
v_status := 'failed';
v_error_message := COALESCE(SQLERRM, 'Unknown error');
RAISE NOTICE ' [UNIT ERROR] unit_id=% sqlstate=% errmsg=%', v_unit.unit_id, SQLSTATE, v_error_message;
INSERT INTO public.group_invoice_details(
id, group_invoice_header_id, unit_id, status, invoice_header_id, error_message, posted_on,
created_on_utc, created_by, company_id
)
VALUES (
gen_random_uuid(), v_group_invoice_header_id, v_unit.unit_id, v_status, v_invoice_header_id,
v_error_message, NOW(), NOW(), v_created_by, v_company_id
);
END;
END LOOP;
UPDATE public.group_invoice_headers
SET success_count = v_success_count,
total_count = v_total_count,
modified_on_utc = NOW(),
modified_by = v_created_by,
error_message = COALESCE(v_error_message, '')
WHERE id = v_group_invoice_header_id;
RAISE NOTICE '[GROUP DONE] group_header_id=% success=% total=%', v_group_invoice_header_id, v_success_count, v_total_count;
UPDATE batch_schedules
SET last_executed_at = NOW()
WHERE id = schedule_record.id;
RAISE NOTICE '[SCHEDULE DONE] schedule_id=% last_executed_at updated', schedule_record.id;
END LOOP;
RAISE NOTICE '[END] run_grouped_invoices(template_id=%)', p_template_id;
END;
$procedure$
-- Procedure: run_sales_invoice_schedule
CREATE OR REPLACE PROCEDURE public.run_sales_invoice_schedule(IN p_execution_date date DEFAULT CURRENT_DATE)
LANGUAGE plpgsql
AS $procedure$
DECLARE
r_template RECORD;
r_detail RECORD;
r_hdr RECORD;
r_det RECORD;
v_expected_execution_date DATE;
v_new_invoice_id UUID;
v_triggered_by UUID; -- <- now resolved from users table
v_log_id UUID;
v_freq text;
v_now_time time := NOW()::time;
v_lock_ok boolean;
BEGIN
SELECT u.id
INTO v_triggered_by
FROM public.users u
WHERE u.is_deleted = false
AND lower(u.first_name) = 'syastem'
ORDER BY u.created_on_utc NULLS LAST, u.id
LIMIT 1;
IF v_triggered_by IS NULL THEN
RAISE NOTICE 'No user found with first_name="Syastem"; using fallback id %', v_triggered_by;
END IF;
FOR r_template IN
SELECT s.*
FROM public.recurring_sales_schedules s
WHERE s.is_deleted = false
AND s.schedule_status = 'active'
AND s.starts_from <= p_execution_date
AND (s.end_date IS NULL OR s.end_date >= p_execution_date)
ORDER BY s.id
LOOP
-- Load one detail row (adapt if you support multiple rows per schedule)
SELECT d.*
INTO r_detail
FROM public.recurrence_schedule_details d
WHERE d.schedule_id = r_template.id
AND d.is_deleted = false
LIMIT 1;
IF NOT FOUND THEN
RAISE NOTICE 'No recurrence detail found for schedule %, skipping.', r_template.id;
CONTINUE;
END IF;
v_freq := lower(r_detail.frequency_cycle);
v_expected_execution_date := NULL;
IF v_freq = 'Daily' THEN
v_expected_execution_date := p_execution_date;
ELSIF v_freq = 'Weekly' THEN
IF r_detail.day_of_week IS NULL THEN
RAISE NOTICE 'Schedule % weekly but day_of_week is NULL; skipping.', r_template.id;
CONTINUE;
END IF;
IF r_detail.day_of_week = TO_CHAR(p_execution_date, 'ID')::int THEN
v_expected_execution_date := p_execution_date;
END IF;
ELSIF v_freq = 'Monthly' THEN
IF r_detail.day_of_month IS NULL THEN
RAISE NOTICE 'Schedule % monthly but day_of_month is NULL; skipping.', r_template.id;
CONTINUE;
END IF;
IF r_detail.day_of_month = EXTRACT(DAY FROM p_execution_date)::int THEN
v_expected_execution_date := p_execution_date;
END IF;
ELSIF v_freq = 'Yearly' THEN
IF r_detail.month_of_year IS NULL OR r_detail.day_of_month IS NULL THEN
RAISE NOTICE 'Schedule % yearly but month/day not set; skipping.', r_template.id;
CONTINUE;
END IF;
IF r_detail.month_of_year = EXTRACT(MONTH FROM p_execution_date)::int
AND r_detail.day_of_month = EXTRACT(DAY FROM p_execution_date)::int THEN
v_expected_execution_date := p_execution_date;
END IF;
ELSE
RAISE NOTICE 'Invalid frequency_cycle "%" on schedule %, skipping.', r_detail.frequency_cycle, r_template.id;
CONTINUE;
END IF;
IF v_expected_execution_date IS NULL THEN
RAISE NOTICE 'Schedule % not aligned for %, skipping.', r_template.id, p_execution_date;
CONTINUE;
END IF;
-- Skip if already executed for this date (defense #1)
IF EXISTS (
SELECT 1 FROM public.schedule_execution_logs l
WHERE l.schedule_id = r_template.id
AND l.execution_date = v_expected_execution_date
AND l.is_deleted = false
) THEN
RAISE NOTICE 'Schedule % already executed for %, skipping.', r_template.id, v_expected_execution_date;
CONTINUE;
END IF;
-- Per-schedule advisory lock (defense #2)
v_lock_ok := pg_try_advisory_lock(hashtextextended(r_template.id::text, 0));
IF NOT v_lock_ok THEN
RAISE NOTICE 'Schedule % locked by another worker, skipping this run.', r_template.id;
CONTINUE;
END IF;
v_log_id := gen_random_uuid();
BEGIN
-- Re-check within the lock (defense #3)
IF EXISTS (
SELECT 1 FROM public.schedule_execution_logs l
WHERE l.schedule_id = r_template.id
AND l.execution_date = v_expected_execution_date
AND l.is_deleted = false
) THEN
RAISE NOTICE 'Schedule % already executed for % (post-lock), skipping.', r_template.id, v_expected_execution_date;
PERFORM pg_advisory_unlock(hashtextextended(r_template.id::text, 0));
CONTINUE;
END IF;
-- Load template header (prefer live, then draft)
SELECT * INTO r_hdr
FROM public.invoice_headers
WHERE id = r_template.invoice_header_id
AND is_deleted = false;
IF NOT FOUND THEN
SELECT * INTO r_hdr
FROM public.draft_invoice_headers
WHERE id = r_template.invoice_header_id
AND is_deleted = false;
END IF;
IF NOT FOUND THEN
INSERT INTO public.schedule_execution_logs (
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
)
VALUES (v_log_id, r_template.id, v_expected_execution_date, NOW(), 'Failure',
'Invoice header not found for template; skipping.', v_triggered_by, false);
RAISE NOTICE 'Invoice header not found for schedule %, skipping.', r_template.id;
PERFORM pg_advisory_unlock(hashtextextended(r_template.id::text, 0));
CONTINUE;
END IF;
-- Create new invoice (draft or committed)
v_new_invoice_id := gen_random_uuid();
IF r_template.default_status = 3 THEN
CALL public.create_invoice_header(
v_new_invoice_id,
r_hdr.company_id,
r_hdr.customer_id,
r_hdr.debit_account_id,
r_hdr.credit_account_id,
p_execution_date,
r_hdr.due_date,
r_hdr.payment_term,
r_hdr.taxable_amount,
r_hdr.cgst_amount,
r_hdr.sgst_amount,
r_hdr.igst_amount,
r_hdr.total_amount,
r_hdr.discount,
r_hdr.fees,
r_hdr.round_off,
r_hdr.currency_id,
r_hdr.note,
3,
v_triggered_by,
r_hdr.invoice_voucher,
r_hdr.source_warehouse_id,
r_hdr.destination_warehouse_id,
r_hdr.so_no,
r_hdr.so_date,
r_hdr.type
);
FOR r_det IN
SELECT *
FROM public.invoice_details
WHERE invoice_header_id = r_hdr.id
LOOP
CALL public.create_invoice_detail(
v_new_invoice_id,
r_det.price,
r_det.quantity,
r_det.discount,
r_det.fees,
r_det.cgst_amount,
r_det.igst_amount,
r_det.sgst_amount,
r_det.taxable_amount,
r_det.total_amount,
r_det.serial_number,
r_det.product_id
);
END LOOP;
ELSE
CALL public.create_draft_invoice_header(
v_new_invoice_id,
r_hdr.company_id,
r_hdr.customer_id,
r_hdr.debit_account_id,
r_hdr.credit_account_id,
p_execution_date,
r_hdr.due_date,
r_hdr.payment_term,
r_hdr.taxable_amount,
r_hdr.cgst_amount,
r_hdr.sgst_amount,
r_hdr.igst_amount,
r_hdr.total_amount,
r_hdr.discount,
r_hdr.fees,
r_hdr.round_off,
r_hdr.currency_id,
r_hdr.note,
1,
v_triggered_by,
r_hdr.invoice_voucher,
r_hdr.source_warehouse_id,
r_hdr.destination_warehouse_id,
r_hdr.so_no,
r_hdr.so_date,
r_hdr.type
);
FOR r_det IN
SELECT *
FROM public.draft_invoice_details
WHERE invoice_header_id = r_hdr.id
LOOP
CALL public.create_draft_invoice_detail(
v_new_invoice_id,
r_det.price,
r_det.quantity,
r_det.discount,
r_det.fees,
r_det.cgst_amount,
r_det.igst_amount,
r_det.sgst_amount,
r_det.taxable_amount,
r_det.total_amount,
r_det.serial_number,
r_det.product_id
);
END LOOP;
END IF;
INSERT INTO public.schedule_execution_logs (
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
)
VALUES (
v_log_id, r_template.id, v_expected_execution_date, NOW(), 'Success', '', v_triggered_by, false
);
RAISE NOTICE 'Executed schedule % for date % successfully (new invoice: %).',
r_template.id, v_expected_execution_date, v_new_invoice_id;
PERFORM pg_advisory_unlock(hashtextextended(r_template.id::text, 0));
EXCEPTION WHEN OTHERS THEN
INSERT INTO public.schedule_execution_logs (
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
)
VALUES (
COALESCE(v_log_id, gen_random_uuid()), r_template.id,
v_expected_execution_date, NOW(), 'Failure', SQLERRM, v_triggered_by, false
);
PERFORM pg_advisory_unlock(hashtextextended(r_template.id::text, 0));
RAISE NOTICE 'Error executing schedule % for date %: %',
r_template.id, v_expected_execution_date, SQLERRM;
END;
END LOOP;
RAISE NOTICE 'Schedule execution completed for date: %', p_execution_date;
END;
$procedure$
-- Procedure: save_company_prefix_for_invoice_and_payment
CREATE OR REPLACE PROCEDURE public.save_company_prefix_for_invoice_and_payment(IN p_company_id uuid, IN p_fin_year integer, IN p_invoice_prefix text, IN p_invoice_length integer, IN p_draft_invoice_prefix text, IN p_draft_invoice_length integer, IN p_group_invoice_prefix text, IN p_group_invoice_length integer, IN p_payment_prefix text, IN p_payment_length integer)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_new_id INTEGER;
BEGIN
-- Handle invoice_header_ids
IF EXISTS (SELECT 1 FROM public.invoice_header_ids WHERE company_id = p_company_id AND fin_year = p_fin_year) THEN
UPDATE public.invoice_header_ids
SET
invoice_prefix = COALESCE(p_invoice_prefix, invoice_prefix),
invoice_length = COALESCE(p_invoice_length, invoice_length)
WHERE company_id = p_company_id AND fin_year = p_fin_year;
ELSE
INSERT INTO public.invoice_header_ids (id, company_id, fin_year, invoice_prefix, invoice_length, last_invoice_id)
VALUES ((SELECT COALESCE(MAX(id), 0) + 1 FROM invoice_header_ids), p_company_id, p_fin_year, p_invoice_prefix, p_invoice_length, 0);
END IF;
-- Handle draft_invoice_header_ids
IF EXISTS (SELECT 1 FROM public.draft_invoice_header_ids WHERE company_id = p_company_id AND fin_year = p_fin_year) THEN
UPDATE public.draft_invoice_header_ids
SET
invoice_prefix = COALESCE(p_draft_invoice_prefix, invoice_prefix),
invoice_length = COALESCE(p_draft_invoice_length, invoice_length)
WHERE company_id = p_company_id AND fin_year = p_fin_year;
ELSE
INSERT INTO public.draft_invoice_header_ids (id, company_id, fin_year, invoice_prefix, invoice_length, last_invoice_id)
VALUES ((SELECT COALESCE(MAX(id), 0) + 1 FROM draft_invoice_header_ids), p_company_id, p_fin_year, p_draft_invoice_prefix, p_draft_invoice_length, 0);
END IF;
-- Handle group_invoice_header_ids
IF EXISTS (SELECT 1 FROM public.group_invoice_header_ids WHERE company_id = p_company_id AND fin_year = p_fin_year) THEN
UPDATE public.group_invoice_header_ids
SET
group_invoice_prefix = COALESCE(p_group_invoice_prefix, group_invoice_prefix),
group_invoice_length = COALESCE(p_group_invoice_length, group_invoice_length)
WHERE company_id = p_company_id AND fin_year = p_fin_year;
ELSE
INSERT INTO public.group_invoice_header_ids (id,company_id, fin_year, group_invoice_prefix, group_invoice_length, last_group_invoice_id)
VALUES ((SELECT COALESCE(MAX(id), 0) + 1 FROM group_invoice_header_ids),p_company_id, p_fin_year, p_group_invoice_prefix, p_group_invoice_length, 0);
END IF;
-- Handle invoice_payment_header_ids
IF EXISTS (SELECT 1 FROM public.invoice_payment_header_ids WHERE company_id = p_company_id AND fin_year = p_fin_year) THEN
UPDATE public.invoice_payment_header_ids
SET
payment_prefix = COALESCE(p_payment_prefix, payment_prefix),
payment_length = COALESCE(p_payment_length, payment_length)
WHERE company_id = p_company_id AND fin_year = p_fin_year;
ELSE
INSERT INTO public.invoice_payment_header_ids (id, company_id, fin_year, payment_prefix, payment_length, last_payment_id)
VALUES ((SELECT COALESCE(MAX(id), 0) + 1 FROM invoice_payment_header_ids), p_company_id, p_fin_year, p_payment_prefix, p_payment_length, 0);
END IF;
END;
$procedure$
-- Procedure: transfer_draft_to_invoice
CREATE OR REPLACE PROCEDURE public.transfer_draft_to_invoice(IN p_draft_invoice_id uuid, IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_draft_invoice_header RECORD;
v_draft_invoice_detail RECORD;
BEGIN
-- Fetch the draft invoice header
SELECT * INTO v_draft_invoice_header
FROM public.draft_invoice_headers
WHERE id = p_draft_invoice_id;
IF v_draft_invoice_header IS NULL THEN
RAISE EXCEPTION 'Draft invoice not found: %', p_draft_invoice_id;
END IF;
-- Call to create invoice header using the procedure
CALL public.create_invoice_header(
v_draft_invoice_header.id,
v_draft_invoice_header.company_id,
v_draft_invoice_header.customer_id,
v_draft_invoice_header.debit_account_id,
v_draft_invoice_header.credit_account_id,
v_draft_invoice_header.invoice_date::date,
v_draft_invoice_header.due_date::date,
v_draft_invoice_header.payment_term,
v_draft_invoice_header.taxable_amount,
v_draft_invoice_header.cgst_amount,
v_draft_invoice_header.sgst_amount,
v_draft_invoice_header.igst_amount,
v_draft_invoice_header.total_amount,
v_draft_invoice_header.discount,
v_draft_invoice_header.fees,
v_draft_invoice_header.round_off,
v_draft_invoice_header.currency_id,
v_draft_invoice_header.note,
v_draft_invoice_header.invoice_status_id,
v_draft_invoice_header.created_by,
v_draft_invoice_header.invoice_voucher,
v_draft_invoice_header.source_warehouse_id,
v_draft_invoice_header.destination_warehouse_id,
v_draft_invoice_header.so_no,
v_draft_invoice_header.so_date::date,
v_draft_invoice_header.type
);
-- Raise a notice to indicate success
RAISE NOTICE 'Invoice header inserted with ID: %', v_draft_invoice_header.id;
-- Fetch and insert invoice details
FOR v_draft_invoice_detail IN
SELECT * FROM public.draft_invoice_details
WHERE invoice_header_id = p_draft_invoice_id
LOOP
INSERT INTO public.invoice_details(
id,
invoice_header_id,
product_id,
price,
quantity,
fees,
discount,
taxable_amount,
sgst_amount,
cgst_amount,
igst_amount,
total_amount,
serial_number)
VALUES(
v_draft_invoice_detail.id,
v_draft_invoice_detail.invoice_header_id,
v_draft_invoice_detail.product_id,
v_draft_invoice_detail.price,
v_draft_invoice_detail.quantity,
v_draft_invoice_detail.fees,
v_draft_invoice_detail.discount,
v_draft_invoice_detail.taxable_amount,
v_draft_invoice_detail.sgst_amount,
v_draft_invoice_detail.cgst_amount,
v_draft_invoice_detail.igst_amount,
v_draft_invoice_detail.total_amount,
v_draft_invoice_detail.serial_number
);
END LOOP;
-- Mark all draft invoice details as deleted after processing
UPDATE public.draft_invoice_details
SET
is_deleted = TRUE,
deleted_on_utc = now()
WHERE invoice_header_id = p_draft_invoice_id;
-- Mark the draft invoice as deleted
UPDATE public.draft_invoice_headers
SET
modified_by = p_modified_by,
is_deleted = TRUE,
deleted_on_utc = now()
WHERE id = p_draft_invoice_id;
END;
$procedure$
-- Procedure: update_account_level_approval_configuration
CREATE OR REPLACE PROCEDURE public.update_account_level_approval_configuration(IN p_company_id uuid, IN p_account_id uuid, IN p_status_id integer, IN p_required_approval_levels integer, IN p_approvals jsonb, IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
approval_item JSONB;
existing_id INT;
BEGIN
-- 1️⃣ Soft delete old records not present in incoming approvals:
UPDATE invoice_approval_users_account
SET is_deleted = TRUE,
deleted_on_utc = NOW(),
modified_on_utc = NOW(),
modified_by = p_modified_by
WHERE company_id = p_company_id
AND account_id = p_account_id
AND NOT EXISTS (
SELECT 1
FROM jsonb_array_elements(p_approvals) AS elem
WHERE (elem->>'userId')::UUID = invoice_approval_users_account.user_id
AND (elem->>'approvalLevel')::INT = invoice_approval_users_account.approval_level
);
-- 2️⃣ Upsert incoming approvals:
FOR approval_item IN SELECT * FROM jsonb_array_elements(p_approvals)
LOOP
SELECT id INTO existing_id
FROM invoice_approval_users_account
WHERE company_id = p_company_id
AND account_id = p_account_id
AND user_id = (approval_item->>'userId')::UUID
AND approval_level = (approval_item->>'approvalLevel')::INT
AND is_deleted = FALSE
LIMIT 1;
IF existing_id IS NOT NULL THEN
UPDATE invoice_approval_users_account
SET status_id = (approval_item->>'statusId')::INT,
modified_on_utc = NOW(),
modified_by = p_modified_by
WHERE id = existing_id;
ELSE
INSERT INTO invoice_approval_users_account (
company_id,
account_id,
status_id,
user_id,
approval_level,
is_deleted,
created_on_utc,
created_by
)
VALUES (
p_company_id,
p_account_id,
(approval_item->>'statusId')::INT,
(approval_item->>'userId')::UUID,
(approval_item->>'approvalLevel')::INT,
FALSE,
NOW(),
p_modified_by
);
END IF;
END LOOP;
-- 3️⃣ Upsert invoice_account_approval_levels:
IF EXISTS (
SELECT 1
FROM invoice_account_approval_levels
WHERE company_id = p_company_id
AND account_id = p_account_id
AND status_id = 2 -- temparary updating only for status 2
) THEN
UPDATE invoice_account_approval_levels
SET approval_level_required = p_required_approval_levels,
status_id = 2,
modified_on_utc = NOW(),
modified_by = p_modified_by
WHERE company_id = p_company_id
AND account_id = p_account_id;
--AND status_id = p_status_id;
ELSE
INSERT INTO invoice_account_approval_levels (
company_id,
account_id,
status_id,
approval_level_required,
created_on_utc,
created_by
)
VALUES (
p_company_id,
p_account_id,
2,
p_required_approval_levels,
NOW(),
p_modified_by
);
END IF;
END;
$procedure$
-- Procedure: update_apartment_invoice_template
CREATE OR REPLACE PROCEDURE public.update_apartment_invoice_template(IN p_calculation_type integer, IN p_company_id uuid, IN p_account_receivable_id uuid, IN p_sales_account_id uuid, IN p_invoice_date date, IN p_starts_from date, IN p_end_date date, IN p_payment_term integer, IN p_currency_id integer, IN p_fixed_product_id uuid, IN p_bhk_product_id uuid, IN p_sqft_product_id uuid, IN p_fixed_product_rate numeric, IN p_bhk_product_rate numeric, IN p_sqft_product_rate numeric, IN p_note character varying, IN p_invoice_status_id integer, IN p_due_days integer, IN p_billing_cycle text, IN p_created_by uuid, IN p_units json, IN p_invoice_template_id uuid, IN p_active_status boolean, IN p_day_of_week integer DEFAULT NULL::integer, IN p_day_of_month integer DEFAULT NULL::integer, IN p_month_of_year integer DEFAULT NULL::integer, IN p_quarters_of_month integer DEFAULT NULL::integer, IN p_half_year integer DEFAULT NULL::integer, IN p_bi_month integer DEFAULT NULL::integer, IN p_time_of_day interval DEFAULT '00:00:00'::interval)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_unit json;
v_unit_id integer;
v_bhk numeric;
v_sqft_area numeric;
v_customer_id uuid;
v_i integer := 0;
v_day_of_week integer := COALESCE(p_day_of_week, 0);
v_day_of_month integer := COALESCE(p_day_of_month, 0);
v_month_of_year integer := COALESCE(p_month_of_year, 0);
v_quarters_of_month integer := COALESCE(p_quarters_of_month, 0);
v_half_year integer := COALESCE(p_half_year, 0);
v_bi_month integer := COALESCE(p_bi_month, 0);
v_time_of_day interval := COALESCE(p_time_of_day, '00:00:00'::interval);
BEGIN
-- Update apartment_invoice_templates
UPDATE public.group_invoice_templates
SET calculation_type_id = p_calculation_type,
account_receivable_id = p_account_receivable_id,
sales_account_id = p_sales_account_id,
invoice_date = p_invoice_date,
starts_from = p_starts_from,
end_date = p_end_date,
grace_period = p_payment_term,
due_days = p_due_days,
fixed_rate = p_fixed_product_rate,
bhk_rate = p_bhk_product_rate,
sqft_rate = p_sqft_product_rate,
invoice_description = p_note,
billing_cycle = p_billing_cycle,
currency_id = p_currency_id,
bhk_product_id = p_bhk_product_id,
fixed_product_id = p_fixed_product_id,
sqft_product_id = p_sqft_product_id,
created_by = p_created_by,
created_on_utc = NOW(),
company_id = p_company_id,
active_status = p_active_status
WHERE id = p_invoice_template_id;
-- Delete existing units related to the invoice template
DELETE FROM public.group_invoice_template_customers
WHERE group_invoice_template_id = p_invoice_template_id;
-- Loop through units JSON array and insert into apartment_invoice_template_units
FOR v_i IN 0 .. json_array_length(p_units) - 1 LOOP
v_unit := p_units->v_i;
v_unit_id := (v_unit->>'id')::integer;
v_bhk := (v_unit->>'bhk')::numeric;
v_sqft_area := (v_unit->>'sqftArea')::numeric;
v_customer_id := (v_unit->>'customerId')::uuid;
-- Check for null values
IF v_customer_id IS NULL THEN
RAISE EXCEPTION 'Customer ID cannot be null';
END IF;
INSERT INTO public.group_invoice_template_customers(
id, group_invoice_template_id, unit_id, bhk, sqft_area, customer_id, created_on_utc, created_by)
VALUES (
gen_random_uuid(), p_invoice_template_id, v_unit_id, v_bhk, v_sqft_area, v_customer_id, NOW(), p_created_by
);
END LOOP;
-- Update or Insert into batch_schedules based on billing cycle
DELETE FROM public.batch_schedules
WHERE group_invoice_template_id = p_invoice_template_id;
IF p_billing_cycle = 'Daily' THEN
-- Daily requires only the time_of_day
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, time_of_day, created_on_utc, created_by)
VALUES (
p_invoice_template_id, p_billing_cycle, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Weekly' THEN
-- Weekly requires day_of_week and time_of_day
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, day_of_week, time_of_day, created_on_utc, created_by)
VALUES (
p_invoice_template_id, p_billing_cycle, v_day_of_week, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Monthly' THEN
-- Requires day_of_month and time_of_day
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_invoice_template_id, p_billing_cycle, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Bi-Monthly' THEN
-- Bi-Monthly requires month_of_year, day_of_month, and time_of_day
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, bi_month, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_invoice_template_id, p_billing_cycle, v_bi_month, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Quarterly' THEN
-- Quarterly requires quarter_of_year, month_of_year, day_of_month, and time_of_day
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, quarters_of_month, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_invoice_template_id, p_billing_cycle, v_quarters_of_month, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycquarters_of_monthle = 'Half-yearly' THEN
-- Half-yearly requires half_year month_of_year, day_of_month, and time_of_day
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, half_year, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_invoice_template_id, p_billing_cycle, v_half_year, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSIF p_billing_cycle = 'Yearly' THEN
-- Yearly requires month_of_year, day_of_month, and time_of_day
INSERT INTO public.batch_schedules(
group_invoice_template_id, billing_cycle, month_of_year, day_of_month, time_of_day, created_on_utc, created_by)
VALUES (
p_invoice_template_id, p_billing_cycle, v_month_of_year, v_day_of_month, v_time_of_day, NOW(), p_created_by
);
ELSE
RAISE EXCEPTION 'Unknown billing cycle: %', p_billing_cycle;
END IF;
END;
$procedure$
-- Procedure: update_company_level_approval_configuration
CREATE OR REPLACE PROCEDURE public.update_company_level_approval_configuration(IN p_company_id uuid, IN p_status_id integer, IN p_required_approval_levels integer, IN p_approvals jsonb, IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
approval_item JSONB;
existing_id INT;
BEGIN
-- 1️⃣ Soft delete old records not present in incoming approvals:
UPDATE invoice_approval_user_company
SET is_deleted = TRUE,
deleted_on_utc = NOW(),
modified_on_utc = NOW(),
modified_by = p_modified_by
WHERE company_id = p_company_id
AND NOT EXISTS (
SELECT 1
FROM jsonb_array_elements(p_approvals) AS elem
WHERE (elem->>'userId')::UUID = invoice_approval_user_company.user_id
AND (elem->>'statusId')::INT = invoice_approval_user_company.status_id
AND (elem->>'approvalLevel')::INT = invoice_approval_user_company.approval_level
);
-- 2️⃣ Upsert incoming approvals:
FOR approval_item IN SELECT * FROM jsonb_array_elements(p_approvals)
LOOP
SELECT id INTO existing_id
FROM invoice_approval_user_company
WHERE company_id = p_company_id
AND user_id = (approval_item->>'userId')::UUID
AND status_id = (approval_item->>'statusId')::INT
AND approval_level = (approval_item->>'approvalLevel')::INT
AND is_deleted = FALSE
LIMIT 1;
IF existing_id IS NOT NULL THEN
UPDATE invoice_approval_user_company
SET status_id = (approval_item->>'statusId')::INT,
modified_on_utc = NOW(),
modified_by = p_modified_by
WHERE id = existing_id;
ELSE
INSERT INTO invoice_approval_user_company (
company_id,
status_id,
user_id,
approval_level,
is_deleted,
created_on_utc,
created_by
)
VALUES (
p_company_id,
(approval_item->>'statusId')::INT,
(approval_item->>'userId')::UUID,
(approval_item->>'approvalLevel')::INT,
FALSE,
NOW(),
p_modified_by
);
END IF;
END LOOP;
-- 3️⃣ Update workflow approval level:
UPDATE invoice_workflow
SET approval_level = p_required_approval_levels
WHERE company_id = p_company_id
-- AND status = p_status_id; -- 🔔 use p_status_id here unless deliberately hardcoded as 2.
AND status = 2; -- 🔔 use p_status_id here unless deliberately hardcoded as 2.
END;
$procedure$
-- Procedure: update_draft_invoice_next_status
CREATE OR REPLACE PROCEDURE public.update_draft_invoice_next_status(IN p_company_id uuid, IN p_invoice_ids uuid[], IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_invoice RECORD; -- Changed variable name from v_draft_invoice to v_invoice for clarity
v_account_id uuid;
v_current_approval_level integer;
v_approval_level_required integer;
v_next_status integer;
v_issue TEXT;
APPROVED_STATUS CONSTANT INTEGER := 3;
v_debug_next_status TEXT;
v_next_temp_id int;
BEGIN
RAISE NOTICE 'START update_draft_invoice_next_status for company %, user %', p_company_id, p_modified_by;
RAISE NOTICE 'Input invoice IDs: %', p_invoice_ids;
-- Loop through each draft invoice in the provided p_invoice_ids
FOR v_invoice IN
SELECT id, invoice_status_id, credit_account_id, current_approval_level
FROM public.draft_invoice_headers
WHERE id = ANY(p_invoice_ids) AND invoice_status_id = 2 -- Assuming status 2 is draft
LOOP
RAISE NOTICE 'Processing invoice ID: %', v_invoice.id;
v_account_id := v_invoice.credit_account_id;
v_current_approval_level := v_invoice.current_approval_level;
-- Get the total approval levels based on invoice status
SELECT approval_level_required
INTO v_approval_level_required
FROM public.invoice_approval_level_view
WHERE company_id = p_company_id
AND status_id = v_invoice.invoice_status_id
AND (
account_id = v_account_id -- Check for account-level entry first
OR account_id IS NULL -- Fallback to company-level entry
)
ORDER BY account_id ASC -- Ensure account-level entry is prioritized
LIMIT 1;
RAISE NOTICE 'Invoice % current approval level: %, required: %', v_invoice.id, v_current_approval_level, v_approval_level_required;
-- If no approval level found, log the issue and skip this invoice
IF v_approval_level_required IS NULL THEN
v_issue := 'Approval level not found';
-- Log the issue into a permanent table
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, 'Not found', v_issue);
RAISE NOTICE 'Invoice % skipped: approval level not found', v_invoice.id;
CONTINUE;
END IF;
-- Check if the user has permission to approve at the next level using the view
PERFORM public.check_invoice_approval_permissions(p_company_id,
v_invoice.invoice_status_id,
p_modified_by,
v_account_id,
(v_invoice.current_approval_level + 1));
-- If not authorized at account level, check company level
IF NOT FOUND THEN
v_issue := 'User not authorized to approve at the next level';
-- Log the issue into a permanent table
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, 'User not authorized', v_issue);
RAISE NOTICE 'Invoice % skipped: user not authorized', v_invoice.id;
CONTINUE;
END IF;
-- Compute next status for debug output
IF v_current_approval_level + 1 < v_approval_level_required THEN
v_debug_next_status := CONCAT('Level ', v_current_approval_level + 1);
ELSE
v_debug_next_status := 'Approved';
END IF;
RAISE NOTICE 'Invoice % next computed status: %', v_invoice.id, v_debug_next_status;
-- Check if the current approval level + 1 is less than the required level
IF v_approval_level_required > (v_current_approval_level + 1) THEN
-- Move to the next approval level
UPDATE public.draft_invoice_headers
SET current_approval_level = (v_current_approval_level + 1),
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_invoice.id;
-- Log the action for this level
INSERT INTO public.invoice_approval_logs(
invoice_id,
status_id,
approved_by,
approved_on,
"comment",
created_on_utc,
created_by,
approval_level
)
VALUES(
v_invoice.id,
v_invoice.invoice_status_id,
p_modified_by,
now(),
CONCAT('Approved at level ', v_current_approval_level + 1),
now(),
p_modified_by,
v_current_approval_level + 1
);
RAISE NOTICE 'Invoice % moved to next approval level: %', v_invoice.id, v_current_approval_level + 1;
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, v_debug_next_status, NULL);
ELSE
-- If the current approval level matches the required level, finalize the invoice
-- Finalize the invoice, move to the final status
UPDATE public.draft_invoice_headers
SET invoice_status_id = APPROVED_STATUS,
modified_by = p_modified_by,
modified_on_utc = now(),
current_approval_level = (v_current_approval_level + 1)
WHERE id = v_invoice.id;
-- Log the final approval action
INSERT INTO public.invoice_approval_logs(
invoice_id,
status_id,
approved_by,
approved_on,
"comment",
created_on_utc,
created_by,
approval_level
)
VALUES(
v_invoice.id,
v_invoice.invoice_status_id,
p_modified_by,
now(),
'Final Approval',
now(),
p_modified_by,
(v_current_approval_level + 1)
);
-- Move draft invoice to final invoice status
CALL transfer_draft_to_invoice(v_invoice.id, p_modified_by);
RAISE NOTICE 'Invoice % approved and transferred', v_invoice.id;
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, v_debug_next_status, NULL);
END IF;
END LOOP;
RAISE NOTICE 'END update_draft_invoice_next_status';
END
$procedure$
-- Procedure: update_draft_invoice_next_status
CREATE OR REPLACE PROCEDURE public.update_draft_invoice_next_status(IN p_company_id uuid, IN p_invoice_status_id integer, IN p_invoice_ids uuid[], IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_invoice RECORD; -- Changed variable name from v_draft_invoice to v_invoice for clarity
v_account_id uuid;
v_current_approval_level integer;
v_approval_level_required integer;
v_next_status integer;
v_has_permission BOOLEAN;
v_issue TEXT;
APPROVED_STATUS CONSTANT INTEGER := 3;
v_debug_next_status varchar;
v_next_temp_id int;
BEGIN
-- Loop through each draft invoice in the provided p_invoice_ids
FOR v_invoice IN
SELECT id, invoice_status_id, credit_account_id, current_approval_level
FROM public.draft_invoice_headers
WHERE id = ANY(p_invoice_ids) AND invoice_status_id = 2 -- Assuming status 2 is draft
LOOP
v_account_id := v_invoice.credit_account_id;
v_current_approval_level := v_invoice.current_approval_level;
-- Get the total approval levels based on invoice status
SELECT approval_level_required
INTO v_approval_level_required
FROM public.invoice_approval_level_view
WHERE company_id = p_company_id
AND status_id = p_invoice_status_id
AND (
account_id = v_account_id -- Check for account-level entry first
OR account_id IS NULL -- Fallback to company-level entry
)
ORDER BY account_id ASC -- Ensure account-level entry is prioritized
LIMIT 1;
-- If no approval level found, log the issue and skip this invoice
IF v_approval_level_required IS NULL THEN
v_issue := 'Approval level not found';
-- Log the issue into a permanent table
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, 'Not found', v_issue);
CONTINUE;
END IF;
-- Check if the user has permission to approve at the next level using the view
SELECT EXISTS (
SELECT 1
FROM public.vw_invoice_approval_permissions
WHERE company_id = p_company_id
AND status_id = p_invoice_status_id
AND user_id = p_modified_by
AND account_approval_level = v_current_approval_level + 1
) INTO v_has_permission;
-- If not authorized at account level, check company level
IF NOT v_has_permission THEN
v_issue := 'User not authorized to approve at the next level';
-- Log the issue into a permanent table
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, 'User not authorized', v_issue);
CONTINUE; -- Skip to the next invoice
END IF;
-- Compute next status for debug output
IF v_current_approval_level + 1 < v_approval_level_required THEN
v_debug_next_status := CONCAT('Level ', v_current_approval_level + 1);
ELSE
v_debug_next_status := 'Approved';
END IF;
-- Check if the current approval level + 1 is less than the required level
IF v_current_approval_level + 1 < v_approval_level_required THEN
-- Move to the next approval level
UPDATE public.draft_invoice_headers
SET current_approval_level = v_current_approval_level + 1,
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_invoice.id;
-- Log the action for this level
INSERT INTO public.invoice_approval_logs(
invoice_id,
status_id,
approved_by,
approved_on,
"comment",
created_on_utc,
created_by,
approval_level
)
VALUES(
v_invoice.id,
p_invoice_status_id,
p_modified_by,
now(),
CONCAT('Approved at level ', v_current_approval_level + 1),
now(),
p_modified_by,
v_current_approval_level + 1
);
RAISE NOTICE 'Invoice % moved to next approval level: %', v_invoice.id, v_current_approval_level + 1;
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, v_debug_next_status, NULL);
ELSE
-- If the current approval level matches the required level, finalize the invoice
-- Finalize the invoice, move to the final status
UPDATE public.draft_invoice_headers
SET invoice_status_id = APPROVED_STATUS,
modified_by = p_modified_by,
modified_on_utc = now(),
current_approval_level = v_current_approval_level + 1
WHERE id = v_invoice.id;
-- Log the final approval action
INSERT INTO public.invoice_approval_logs(
invoice_id,
status_id,
approved_by,
approved_on,
"comment",
created_on_utc,
created_by,
approval_level
)
VALUES(
v_invoice.id,
p_invoice_status_id,
p_modified_by,
now(),
'Final Approval',
now(),
p_modified_by,
v_current_approval_level + 1
);
-- Move draft invoice to final invoice status
CALL transfer_draft_to_invoice(v_invoice.id, p_modified_by);
RAISE NOTICE 'Invoice % approved and transferred', v_invoice.id;
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status (id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, v_debug_next_status, NULL);
END IF;
END LOOP;
END
$procedure$
-- Procedure: update_invoice_approval_user
CREATE OR REPLACE PROCEDURE public.update_invoice_approval_user(IN p_updates jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
update_item jsonb;
record_exists int;
BEGIN
FOR update_item IN SELECT * FROM jsonb_array_elements(p_updates)
LOOP
-- Check if record exists by ID (even if soft-deleted)
SELECT COUNT(*) INTO record_exists
FROM public.invoice_approval_user_company
WHERE id = (update_item->>'Id')::int;
IF record_exists > 0 THEN
-- Update existing record and reset is_deleted = false if needed
UPDATE public.invoice_approval_user_company
SET
company_id = (update_item->>'CompanyId')::uuid,
status_id = (update_item->>'StatusId')::int,
user_id = (update_item->>'UserId')::uuid,
approval_level = (update_item->>'ApprovalLevel')::int,
modified_by = (update_item->>'ModifiedBy')::uuid,
modified_on_utc = NOW(),
is_deleted = false
WHERE id = (update_item->>'Id')::int;
ELSE
-- Insert new record
INSERT INTO public.invoice_approval_user_company (
company_id,
status_id,
user_id,
approval_level,
created_on_utc,
modified_on_utc,
deleted_on_utc,
is_deleted,
created_by,
modified_by
) VALUES (
(update_item->>'CompanyId')::uuid,
(update_item->>'StatusId')::int,
(update_item->>'UserId')::uuid,
(update_item->>'ApprovalLevel')::int,
NOW(),
NULL,
NULL,
FALSE,
(update_item->>'ModifiedBy')::uuid,
NULL
);
END IF;
END LOOP;
END;
$procedure$
-- Procedure: update_invoice_company_approval_user
CREATE OR REPLACE PROCEDURE public.update_invoice_company_approval_user(IN p_updates jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
update_item jsonb;
record_exists int;
BEGIN
FOR update_item IN SELECT * FROM jsonb_array_elements(p_updates)
LOOP
-- Check if record exists regardless of is_deleted
SELECT COUNT(*) INTO record_exists
FROM public.invoice_approval_user_company
WHERE id = (update_item->>'Id')::int;
IF record_exists > 0 THEN
-- Update record, remove is_deleted condition and optionally reset is_deleted to false
UPDATE public.invoice_approval_user_company
SET
company_id = (update_item->>'CompanyId')::uuid,
status_id = (update_item->>'StatusId')::int,
user_id = (update_item->>'UserId')::uuid,
approval_level = (update_item->>'ApprovalLevel')::int,
modified_by = (update_item->>'ModifiedBy')::uuid,
modified_on_utc = now(),
is_deleted = false -- optional: reactivate if was deleted
WHERE id = (update_item->>'Id')::int;
ELSE
-- Insert new record
INSERT INTO public.invoice_approval_user_company
(
company_id,
status_id,
user_id,
created_on_utc,
modified_on_utc,
deleted_on_utc,
is_deleted,
created_by,
modified_by,
approval_level
) VALUES (
(update_item->>'CompanyId')::uuid,
(update_item->>'StatusId')::int,
(update_item->>'UserId')::uuid,
now(),
null,
null,
false,
(update_item->>'ModifiedBy')::uuid,
null,
(update_item->>'ApprovalLevel')::int
);
END IF;
END LOOP;
END;
$procedure$
-- Procedure: upsert_invoice_status_company_config_json
CREATE OR REPLACE PROCEDURE public.upsert_invoice_status_company_config_json(IN p_updates jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_item JSONB;
BEGIN
FOR v_item IN SELECT * FROM jsonb_array_elements(p_updates)
LOOP
PERFORM public.upsert_invoice_status_company_config(
(v_item->>'CompanyId')::UUID,
(v_item->>'StatusId')::INT,
(v_item->>'IsEnabled')::BOOLEAN,
(v_item->>'UserId')::UUID
);
END LOOP;
END;
$procedure$
-- Procedure: update_invoice_next_status
CREATE OR REPLACE PROCEDURE public.update_invoice_next_status(IN p_company_id uuid, IN p_invoice_status_id integer, IN p_invoice_ids uuid[], IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_draft_invoice RECORD;
v_next_status integer;
v_account_id uuid;
v_has_account_workflow BOOLEAN;
v_has_user_account_level_approval BOOLEAN;
v_has_user_company_level_approval BOOLEAN;
v_is_data_available_for_company BOOLEAN;
APPROVED_STATUS CONSTANT INTEGER := 3;
FIRST_LEVEL_APPROVAL CONSTANT INTEGER := 8;
SECOND_LEVEL_APPROVAL CONSTANT INTEGER := 9;
BEGIN
FOR v_draft_invoice IN
SELECT id, invoice_status_id,credit_account_id
FROM public.draft_invoice_headers
WHERE id = ANY(p_invoice_ids)
LOOP
v_account_id := v_draft_invoice.credit_account_id;
SELECT EXISTS (
SELECT 1 FROM public.invoice_account_workflows
WHERE company_id = p_company_id
AND account_id = v_account_id
AND status = v_draft_invoice.invoice_status_id
AND is_deleted = false
) INTO v_has_account_workflow;
IF v_has_account_workflow THEN
SELECT next_status INTO v_next_status
FROM public.invoice_account_workflows
WHERE company_id = p_company_id
AND account_id = v_account_id
AND status = v_draft_invoice.invoice_status_id
AND is_deleted = false
LIMIT 1;
SELECT EXISTS (
SELECT 1 FROM public.invoice_account_approval_users
WHERE company_id = p_company_id
AND account_id = v_account_id
AND status = v_next_status
AND user_id = p_modified_by
) INTO v_has_user_account_level_approval;
IF NOT v_has_user_account_level_approval THEN
RAISE EXCEPTION 'User % is not authorized to approve at this level for account % ', p_modified_by, v_account_id ;
EXIT;
END IF;
ELSE
v_next_status := p_invoice_status_id;
SELECT EXISTS (
SELECT 1 FROM public.invoice_approvals
WHERE company_id = p_company_id
) INTO v_is_data_available_for_company;
IF v_is_data_available_for_company THEN
SELECT EXISTS (
SELECT 1 FROM public.invoice_approvals
WHERE company_id = p_company_id
AND status_id = v_next_status
AND user_id = p_modified_by
) INTO v_has_user_company_level_approval;
IF NOT v_has_user_company_level_approval THEN
RAISE EXCEPTION 'User % is not authorized to approve at this level', p_modified_by;
EXIT;
END IF;
END IF;
END IF;
UPDATE public.draft_invoice_headers
SET invoice_status_id = v_next_status,
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_draft_invoice.id;
IF v_next_status = APPROVED_STATUS OR v_next_status = FIRST_LEVEL_APPROVAL OR v_next_status = SECOND_LEVEL_APPROVAL THEN
INSERT INTO public.invoice_approval_logs(
invoice_id,
status_id,
approved_by,
approved_on,
"comment",
created_on_utc,
created_by
)
VALUES(
v_draft_invoice.id,
v_next_status,
p_modified_by,
now(),
CASE
WHEN v_next_status = FIRST_LEVEL_APPROVAL THEN 'Level 1 Approval'
WHEN v_next_status = SECOND_LEVEL_APPROVAL THEN 'Level 2 Approval'
WHEN v_next_status = APPROVED_STATUS THEN 'Final Approval'
ELSE 'Status updated'
END,
now(),
p_modified_by
);
END IF;
IF p_invoice_status_id = APPROVED_STATUS THEN
CALL transfer_draft_to_invoice(v_draft_invoice.id, p_modified_by);
RAISE NOTICE 'Draft Invoice Approved. Transferring to Final Invoice: %', v_draft_invoice.id;
END IF;
END LOOP;
END
$procedure$
-- Procedure: update_invoice_next_status_for_invoice_header
CREATE OR REPLACE PROCEDURE public.update_invoice_next_status_for_invoice_header(IN p_company_id uuid, IN p_invoice_ids uuid[], IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_invoice RECORD; -- Variable name for invoice headers
v_account_id uuid;
v_next_status integer;
v_next_status_name character varying;
v_issue TEXT;
v_next_temp_id int;
v_has_permission BOOLEAN; -- Permission check variable
BEGIN
RAISE NOTICE 'START update_invoice_next_status_for_invoice_header: company_id=%, invoice_ids=%, modified_by=%', p_company_id, p_invoice_ids, p_modified_by;
-- Loop through each invoice in the provided p_invoice_ids
FOR v_invoice IN
SELECT id, invoice_status_id, credit_account_id
FROM public.invoice_headers
WHERE id = ANY(p_invoice_ids)
LOOP
RAISE NOTICE 'Processing invoice ID: %', v_invoice.id;
v_account_id := v_invoice.credit_account_id;
-- Get the next status from invoice workflow
RAISE NOTICE 'Fetching next status for invoice ID: %, company_id: %, current status: %', v_invoice.id, p_company_id, v_invoice.invoice_status_id;
SELECT next_status INTO v_next_status
FROM public.invoice_workflow
WHERE company_id = p_company_id
AND status = v_invoice.invoice_status_id
AND is_deleted = false
AND is_enabled = true
LIMIT 1;
-- If no next status found, log the issue and skip this invoice
IF v_next_status IS NULL THEN
v_issue := 'Next status not found for company % and status %';
RAISE NOTICE 'Next status not found for company % and status %', p_company_id, v_invoice.invoice_status_id;
-- Log the issue into a permanent table
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, 'Not found', v_issue);
CONTINUE;
END IF;
RAISE NOTICE 'Next status for invoice ID %: %', v_invoice.id, v_next_status;
-- Check if the user has permission to move to the next status using the updated function
RAISE NOTICE 'Checking if user has permission to move invoice ID % to next status %', v_invoice.id, v_next_status;
-- Calling the updated check_invoice_approval_permissions function to check permission
PERFORM public.check_invoice_approval_permissions(
p_company_id,
v_next_status,
p_modified_by,
v_account_id,0);
-- Check if the permission was granted (if any row is returned)
IF NOT FOUND THEN
v_issue := 'User % is not authorized to move invoice to the next status for status %';
RAISE NOTICE 'User % is not authorized to move invoice ID % to next status %', p_modified_by, v_invoice.id, v_next_status;
-- Log the issue into a permanent table
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, 'User not authorized', v_issue);
CONTINUE; -- Skip this invoice and move to the next one
END IF;
-- Move to the next status
RAISE NOTICE 'Moving invoice ID % to the next status: %', v_invoice.id, v_next_status;
UPDATE public.invoice_headers
SET invoice_status_id = v_next_status, -- Update the status to the next status
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_invoice.id;
-- Log the action for moving to the next status
INSERT INTO public.invoice_approval_logs(
invoice_id,
status_id,
approved_by,
approved_on,
"comment",
created_on_utc,
created_by,
approval_level
)
VALUES(
v_invoice.id,
v_invoice.invoice_status_id,
p_modified_by,
now(),
'Approved and moved to next status', -- Updated comment
now(),
p_modified_by,
0 -- Setting approval_level to 0, as no level change is needed
);
-- Get the name of the next status for debug output
SELECT name INTO v_next_status_name
FROM public.invoice_statuses
WHERE id = v_next_status;
-- Insert into temp_invoice_next_status for debugging purposes
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, v_next_status_name, NULL);
END LOOP;
RAISE NOTICE 'END update_invoice_next_status_for_invoice_header';
END
$procedure$
-- Procedure: update_invoice_next_status_for_invoice_header
CREATE OR REPLACE PROCEDURE public.update_invoice_next_status_for_invoice_header(IN p_company_id uuid, IN p_invoice_status_id integer, IN p_invoice_ids uuid[], IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_invoice RECORD; -- Variable name for invoice headers
v_account_id uuid;
v_next_status integer;
v_next_status_name character varying;
v_has_permission BOOLEAN;
v_issue TEXT;
v_next_temp_id int;
BEGIN
-- Loop through each invoice in the provided p_invoice_ids
FOR v_invoice IN
SELECT id, invoice_status_id, credit_account_id
FROM public.invoice_headers
WHERE id = ANY(p_invoice_ids)
LOOP
v_account_id := v_invoice.credit_account_id;
-- Get the next status from invoice workflow
SELECT next_status INTO v_next_status
FROM public.invoice_workflow
WHERE company_id = p_company_id
AND status = p_invoice_status_id
AND is_deleted = false
AND is_enabled = true
LIMIT 1;
-- If no next status found, log the issue and skip this invoice
IF v_next_status IS NULL THEN
v_issue := 'Next status not found for company % and status %';
-- Log the issue into a permanent table
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, 'Not found', v_issue);
CONTINUE;
END IF;
-- Check if the user has permission to move to the next status using the view
SELECT EXISTS (
SELECT 1
FROM public.vw_invoice_approval_permissions
WHERE company_id = p_company_id
AND status_id = v_next_status -- Check for the next status
AND user_id = p_modified_by
) INTO v_has_permission;
-- If not authorized to move to the next status, log the issue and skip the invoice
IF NOT v_has_permission THEN
v_issue := 'User % is not authorized to move invoice to the next status for status %';
-- Log the issue into a permanent table
INSERT INTO public.invoice_approval_issue_log(invoice_id, issue, created_on_utc, created_by)
VALUES (v_invoice.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, 'User not authorized', v_issue);
CONTINUE; -- Skip this invoice and move to the next one
END IF;
-- Move to the next status
UPDATE public.invoice_headers
SET invoice_status_id = v_next_status, -- Update the status to the next status
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_invoice.id;
-- Log the action for moving to the next status
INSERT INTO public.invoice_approval_logs(
invoice_id,
status_id,
approved_by,
approved_on,
"comment",
created_on_utc,
created_by,
approval_level
)
VALUES(
v_invoice.id,
p_invoice_status_id,
p_modified_by,
now(),
'Approved and moved to next status', -- Updated comment
now(),
p_modified_by,
0 -- Setting approval_level to 0, as no level change is needed
);
SELECT name INTO v_next_status_name
FROM public.invoice_statuses
WHERE id = v_next_status;
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_invoice_next_status;
INSERT INTO temp_invoice_next_status(id, invoice_id, status, error)
VALUES (v_next_temp_id, v_invoice.id, v_next_status_name, NULL);
END LOOP;
END
$procedure$
-- Procedure: update_invoice_previous_status
CREATE OR REPLACE PROCEDURE public.update_invoice_previous_status(IN p_company_id uuid, IN p_invoice_status_id integer, IN p_invoice_ids uuid[], IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_invoice RECORD;
v_minimum_status INTEGER := 0; -- Local variable for minimum status
BEGIN
-- Process invoices for previous status update
FOR v_invoice IN
SELECT id, invoice_status_id
FROM public.draft_invoice_headers
WHERE id = ANY(p_invoice_ids)
LOOP
-- Ensure the status doesn't go below the minimum allowed status
IF v_invoice.invoice_status_id > v_minimum_status THEN
-- Update the invoice to the previous status
UPDATE public.draft_invoice_headers
SET invoice_status_id = p_invoice_status_id,
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_invoice.id;
RAISE NOTICE 'Invoice status updated to previous status for Invoice ID: %', v_invoice.id;
ELSE
RAISE NOTICE 'Cannot decrement status below the minimum allowed status for Invoice ID: %', v_invoice.id;
END IF;
END LOOP;
END
$procedure$
-- Procedure: clean_up_org_sales
CREATE OR REPLACE PROCEDURE public.clean_up_org_sales(IN p_organization_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_company_id uuid;
v_customer_id uuid;
v_customer_note_header_id uuid;
v_invoice_header_id uuid;
v_invoice_payment_header_id uuid;
v_draft_invoice_header_id uuid;
v_group_invoice_header_id uuid;
v_gate_pass_header_id uuid;
v_user_id uuid;
BEGIN
FOR v_company_id IN
SELECT id FROM companies WHERE organization_id = p_organization_id
LOOP
-- Customers and all their detail tables
FOR v_customer_id IN SELECT id FROM customers WHERE company_id = v_company_id LOOP
DELETE FROM customer_contacts WHERE customer_id = v_customer_id;
DELETE FROM customer_bank_accounts WHERE customer_id = v_customer_id;
DELETE FROM customer_upis WHERE customer_id = v_customer_id;
DELETE FROM customer_default_accounts WHERE customer_id = v_customer_id;
FOR v_customer_note_header_id IN SELECT id FROM customer_note_headers WHERE customer_id = v_customer_id LOOP
DELETE FROM customer_note_details WHERE customer_note_header_id = v_customer_note_header_id;
END LOOP;
DELETE FROM customer_note_headers WHERE customer_id = v_customer_id;
END LOOP;
DELETE FROM customers WHERE company_id = v_company_id;
DELETE FROM customer_note_statuses WHERE created_by = v_company_id OR modified_by = v_company_id;
DELETE FROM customer_note_work_flows WHERE company_id = v_company_id;
-- Draft Invoices and their details
FOR v_draft_invoice_header_id IN SELECT id FROM draft_invoice_headers WHERE company_id = v_company_id LOOP
DELETE FROM draft_invoice_details WHERE invoice_header_id = v_draft_invoice_header_id;
END LOOP;
DELETE FROM draft_invoice_headers WHERE company_id = v_company_id;
DELETE FROM draft_invoice_header_ids WHERE company_id = v_company_id;
-- Group Invoices and their details/templates
FOR v_group_invoice_header_id IN SELECT id FROM group_invoice_headers WHERE company_id = v_company_id LOOP
DELETE FROM group_invoice_details WHERE group_invoice_header_id = v_group_invoice_header_id;
END LOOP;
DELETE FROM group_invoice_headers WHERE company_id = v_company_id;
DELETE FROM group_invoice_header_ids WHERE company_id = v_company_id;
DELETE FROM group_invoice_templates WHERE company_id = v_company_id;
DELETE FROM group_invoice_template_customers WHERE customer_id IN (
SELECT id FROM customers WHERE company_id = v_company_id
);
-- Invoice Headers and all their details
FOR v_invoice_header_id IN SELECT id FROM invoice_headers WHERE company_id = v_company_id LOOP
DELETE FROM invoice_details WHERE invoice_header_id = v_invoice_header_id;
DELETE FROM invoice_approval_logs WHERE invoice_id = v_invoice_header_id;
DELETE FROM invoice_approval_issue_log WHERE invoice_id = v_invoice_header_id;
DELETE FROM invoice_penalties WHERE invoice_header_id = v_invoice_header_id;
DELETE FROM penalty_processing_logs WHERE invoice_id = v_invoice_header_id;
DELETE FROM recurring_sales_schedules WHERE invoice_header_id = v_invoice_header_id;
DELETE FROM group_invoice_details WHERE invoice_header_id = v_invoice_header_id;
END LOOP;
DELETE FROM invoice_headers WHERE company_id = v_company_id;
DELETE FROM invoice_header_ids WHERE company_id = v_company_id;
-- Invoice payments and their details
FOR v_invoice_payment_header_id IN SELECT id FROM invoice_payment_headers WHERE company_id = v_company_id LOOP
DELETE FROM invoice_payment_details WHERE invoice_payment_header_id = v_invoice_payment_header_id;
END LOOP;
DELETE FROM invoice_payment_headers WHERE company_id = v_company_id;
DELETE FROM invoice_payment_header_ids WHERE company_id = v_company_id;
-- Invoice workflow/configs
DELETE FROM invoice_workflow WHERE company_id = v_company_id;
DELETE FROM invoice_status_company_configs WHERE company_id = v_company_id;
DELETE FROM invoice_account_approval_levels WHERE company_id = v_company_id;
DELETE FROM invoice_approval_users_account WHERE company_id = v_company_id;
DELETE FROM invoice_approval_user_company WHERE company_id = v_company_id;
-- Invoice voucher ids
DELETE FROM invoice_voucher_ids WHERE company_id = v_company_id;
-- Penalties and configs
DELETE FROM penalty_configs WHERE company_id = v_company_id;
DELETE FROM penalty_frequencies WHERE created_by = v_company_id OR modified_by = v_company_id;
-- Warehouses (corrected)
DELETE FROM warehouses WHERE customer_id IN (
SELECT id FROM customers WHERE company_id = v_company_id
);
-- Gate pass and their details
FOR v_gate_pass_header_id IN SELECT id FROM gate_pass_headers WHERE company_id = v_company_id LOOP
DELETE FROM gate_pass_details WHERE gate_pass_header_id = v_gate_pass_header_id;
END LOOP;
DELETE FROM gate_pass_headers WHERE company_id = v_company_id;
-- Users and user roles
FOR v_user_id IN SELECT id FROM users WHERE company_id = v_company_id LOOP
DELETE FROM user_roles WHERE user_id = v_user_id;
END LOOP;
DELETE FROM users WHERE company_id = v_company_id;
-- Finally, company itself
DELETE FROM companies WHERE id = v_company_id;
END LOOP;
-- Organization record itself
DELETE FROM organizations WHERE id = p_organization_id;
RAISE NOTICE 'Organization % and all related sales data deleted.', p_organization_id;
END;
$procedure$
-- Procedure: copy_draft_invoices
CREATE OR REPLACE PROCEDURE public.copy_draft_invoices(IN draft_invoice_ids uuid[])
LANGUAGE plpgsql
AS $procedure$
DECLARE
original_draft_invoice RECORD;
original_draft_detail RECORD;
new_draft_header_id UUID;
BEGIN
-- Loop through each provided draft invoice ID
FOR original_draft_invoice IN
SELECT * FROM draft_invoice_headers WHERE id = ANY(draft_invoice_ids) AND is_deleted = FALSE
LOOP
RAISE NOTICE 'Processing draft invoice ID: %', original_draft_invoice.id;
-- Generate a new UUID for the draft invoice header
new_draft_header_id := gen_random_uuid();
RAISE NOTICE 'Generated new draft invoice header ID: %', new_draft_header_id;
-- Call the create_draft_invoice_header procedure to create the new draft invoice header
CALL create_draft_invoice_header(
new_draft_header_id,
original_draft_invoice.company_id,
original_draft_invoice.customer_id,
original_draft_invoice.debit_account_id,
original_draft_invoice.credit_account_id,
original_draft_invoice.invoice_date::date,
original_draft_invoice.due_date::date,
original_draft_invoice.payment_term,
original_draft_invoice.taxable_amount,
original_draft_invoice.cgst_amount,
original_draft_invoice.sgst_amount,
original_draft_invoice.igst_amount,
original_draft_invoice.total_amount,
original_draft_invoice.discount,
original_draft_invoice.fees,
original_draft_invoice.round_off,
original_draft_invoice.currency_id,
original_draft_invoice.note,
1, -- Set draft status ID
original_draft_invoice.created_by,
original_draft_invoice.invoice_voucher,
original_draft_invoice.source_warehouse_id,
original_draft_invoice.destination_warehouse_id,
original_draft_invoice.so_no,
original_draft_invoice.so_date::date,
original_draft_invoice.type
);
RAISE NOTICE 'Inserted new draft invoice header for original draft invoice ID: %', original_draft_invoice.id;
-- Loop through each detail for the current draft invoice and copy to new draft
FOR original_draft_detail IN
SELECT * FROM draft_invoice_details WHERE invoice_header_id = original_draft_invoice.id
LOOP
RAISE NOTICE 'Copying draft detail with serial number % for draft invoice ID: %', original_draft_detail.serial_number, original_draft_invoice.id;
-- Call create_invoice_detail to insert each detail
CALL create_draft_invoice_detail(
new_draft_header_id, -- Use the new draft invoice header ID
original_draft_detail.price,
original_draft_detail.quantity,
original_draft_detail.discount,
original_draft_detail.fees,
original_draft_detail.cgst_amount,
original_draft_detail.igst_amount,
original_draft_detail.sgst_amount,
original_draft_detail.taxable_amount,
original_draft_detail.total_amount,
original_draft_detail.serial_number,
original_draft_detail.product_id
);
RAISE NOTICE 'Inserted new draft invoice detail for draft header ID: %', new_draft_header_id;
END LOOP;
END LOOP;
END;
$procedure$
-- Procedure: copy_invoices
CREATE OR REPLACE PROCEDURE public.copy_invoices(IN p_invoice_ids uuid[])
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_invoice_header RECORD;
v_invoice_detail RECORD;
v_new_header_id UUID;
-- Constants for statuses and other values
invoice_status_draft CONSTANT INTEGER := 1;
approved_invoice_status CONSTANT INTEGER := 3;
BEGIN
-- Loop through each provided invoice ID
FOR v_invoice_header IN
(
SELECT id, company_id, customer_id, debit_account_id, credit_account_id, invoice_date, due_date,
payment_term, taxable_amount, cgst_amount, sgst_amount, igst_amount, total_amount, discount,
fees, round_off, currency_id, note, created_by, invoice_voucher, source_warehouse_id,
destination_warehouse_id, so_no, so_date, type,invoice_status_id
FROM draft_invoice_headers
WHERE id = ANY(p_invoice_ids) AND is_deleted = false
UNION ALL
SELECT id, company_id, customer_id, debit_account_id, credit_account_id, invoice_date, due_date,
payment_term, taxable_amount, cgst_amount, sgst_amount, igst_amount, total_amount, discount,
fees, round_off, currency_id, note, created_by, invoice_voucher, source_warehouse_id,
destination_warehouse_id, so_no, so_date, type,invoice_status_id
FROM invoice_headers
WHERE id = ANY(p_invoice_ids)
)
LOOP
RAISE NOTICE 'Invoices id: %', v_invoice_header.id;
-- Generate a new UUID for the new invoice header
v_new_header_id := gen_random_uuid();
RAISE NOTICE 'Generated new invoice header ID: %', v_new_header_id;
-- Call the create_draft_invoice_header procedure to create the new invoice header
CALL create_draft_invoice_header(
v_new_header_id,
v_invoice_header.company_id,
v_invoice_header.customer_id,
v_invoice_header.debit_account_id,
v_invoice_header.credit_account_id,
v_invoice_header.invoice_date::date,
v_invoice_header.due_date::date,
v_invoice_header.payment_term,
v_invoice_header.taxable_amount,
v_invoice_header.cgst_amount,
v_invoice_header.sgst_amount,
v_invoice_header.igst_amount,
v_invoice_header.total_amount,
v_invoice_header.discount,
v_invoice_header.fees,
v_invoice_header.round_off,
v_invoice_header.currency_id,
v_invoice_header.note,
invoice_status_draft,
v_invoice_header.created_by,
v_invoice_header.invoice_voucher,
v_invoice_header.source_warehouse_id,
v_invoice_header.destination_warehouse_id,
v_invoice_header.so_no,
v_invoice_header.so_date::date,
v_invoice_header.type
);
RAISE NOTICE 'Inserted new invoice header for original invoice ID: %', v_invoice_header.id;
-- Loop through details based on whether the invoice is draft or not
IF v_invoice_header.invoice_status_id < approved_invoice_status THEN
FOR v_invoice_detail IN
SELECT * FROM draft_invoice_details WHERE invoice_header_id = v_invoice_header.id
LOOP
RAISE NOTICE 'Copying draft detail with serial number % for draft invoice ID: %', v_invoice_detail.serial_number, v_invoice_header.id;
-- Call create_draft_invoice_detail to insert each detail
CALL create_draft_invoice_detail(
v_new_header_id,
v_invoice_detail.price,
v_invoice_detail.quantity,
v_invoice_detail.discount,
v_invoice_detail.fees,
v_invoice_detail.cgst_amount,
v_invoice_detail.igst_amount,
v_invoice_detail.sgst_amount,
v_invoice_detail.taxable_amount,
v_invoice_detail.total_amount,
v_invoice_detail.serial_number,
v_invoice_detail.product_id
);
END LOOP;
ELSE
FOR v_invoice_detail IN
SELECT * FROM invoice_details WHERE invoice_header_id = v_invoice_header.id
LOOP
RAISE NOTICE 'Copying detail with serial number % for invoice ID: %', v_invoice_detail.serial_number, v_invoice_header.id;
-- Call create_draft_invoice_detail to insert each detail
CALL create_draft_invoice_detail(
v_new_header_id,
v_invoice_detail.price,
v_invoice_detail.quantity,
v_invoice_detail.discount,
v_invoice_detail.fees,
v_invoice_detail.cgst_amount,
v_invoice_detail.igst_amount,
v_invoice_detail.sgst_amount,
v_invoice_detail.taxable_amount,
v_invoice_detail.total_amount,
v_invoice_detail.serial_number,
v_invoice_detail.product_id
);
RAISE NOTICE 'Inserted detail for new invoice header ID: %', v_new_header_id;
END LOOP;
END IF;
END LOOP;
RAISE NOTICE 'Procedure completed successfully.';
END;
$procedure$
-- Procedure: create_invoice_detail
CREATE OR REPLACE PROCEDURE public.create_invoice_detail(IN p_invoice_header_id uuid, IN p_price numeric, IN p_quantity numeric, IN p_discount numeric, IN p_fees numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_sgst_amount numeric, IN p_taxable_amount numeric, IN p_total_amount numeric, IN p_serial_number integer, IN p_product_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
p_id uuid;
BEGIN
p_id := gen_random_uuid();
INSERT INTO public.invoice_details(
id,
invoice_header_id,
price,
quantity,
cgst_amount,
discount,
fees,
igst_amount,
sgst_amount,
taxable_amount,
total_amount,
serial_number,
product_id
)
VALUES (
p_id,
p_invoice_header_id,
p_price,
p_quantity,
p_cgst_amount,
p_discount,
p_fees,
p_igst_amount,
p_sgst_amount,
p_taxable_amount,
p_total_amount,
p_serial_number,
p_product_id
);
END;
$procedure$
-- Procedure: create_invoice_header
CREATE OR REPLACE PROCEDURE public.create_invoice_header(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_invoice_date date, IN p_due_date date, IN p_payment_term integer, IN p_taxable_amount numeric, IN p_cgst_amount numeric, IN p_sgst_amount numeric, IN p_igst_amount numeric, IN p_total_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_round_off numeric, IN p_currency_id integer, IN p_note character varying, IN p_invoice_status_id integer, IN p_created_by uuid, IN p_invoice_voucher character varying, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_so_no character varying, IN p_so_date date, IN p_type integer)
LANGUAGE plpgsql
AS $procedure$
DECLARE
invoice_number character varying;
BEGIN
invoice_number := get_new_invoice_number(p_company_id, p_invoice_date);
RAISE NOTICE 'Value: %', invoice_number;
INSERT INTO
public.invoice_headers(
id,
company_id,
customer_id,
credit_account_id,
debit_account_id,
invoice_number,
invoice_date,
due_date,
payment_term,
taxable_amount,
cgst_amount,
sgst_amount,
igst_amount,
total_amount,
discount,
fees,
round_off,
currency_id,
note,
invoice_status_id,
created_by,
invoice_voucher,
source_warehouse_id,
destination_warehouse_id,
created_on_utc,
so_no,
so_date,
type
)
VALUES (
p_id,
p_company_id,
p_customer_id,
p_credit_account_id,
p_debit_account_id,
invoice_number,
p_invoice_date,
p_due_date,
p_payment_term,
p_taxable_amount,
p_cgst_amount,
p_sgst_amount,
p_igst_amount,
p_total_amount,
p_discount,
p_fees,
p_round_off,
p_currency_id,
p_note,
p_invoice_status_id,
p_created_by,
p_invoice_voucher,
p_source_warehouse_id,
p_destination_warehouse_id,
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
p_so_no,
p_so_date,
p_type
);
END;
$procedure$
-- Procedure: edit_draft_invoice
CREATE OR REPLACE PROCEDURE public.edit_draft_invoice(IN p_invoice_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_payment_term integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_round_off numeric, IN p_note text, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_modified_by uuid, IN p_invoice_details jsonb, IN p_type integer)
LANGUAGE plpgsql
AS $procedure$
DECLARE
detail RECORD; -- Record variable for each detail from the JSONB array
BEGIN
-- Check if the invoice exists and can be edited (status < 3)
IF EXISTS (
SELECT 1
FROM public.draft_invoice_headers
WHERE id = p_invoice_id
AND invoice_status_id < 3
) THEN
-- Update the draft invoice header
UPDATE public.draft_invoice_headers
SET
company_id = p_company_id,
customer_id = p_customer_id,
currency_id = p_currency_id,
invoice_date = p_invoice_date,
payment_term = p_payment_term,
due_date = p_due_date,
total_amount = p_total_amount,
taxable_amount = p_taxable_amount,
discount = p_discount,
fees = p_fees,
sgst_amount = p_sgst_amount,
cgst_amount = p_cgst_amount,
igst_amount = p_igst_amount,
round_off = p_round_off,
note = p_note,
source_warehouse_id = p_source_warehouse_id, -- New Parameter
destination_warehouse_id = p_destination_warehouse_id, -- New Parameter
modified_on_utc = now(),
modified_by = p_modified_by,
type = p_type
WHERE id = p_invoice_id;
-- Soft delete existing invoice details
UPDATE public.draft_invoice_details
SET is_deleted = true, deleted_on_utc = now()
WHERE invoice_header_id = p_invoice_id;
-- Insert or update the new invoice details from the JSONB
FOR detail IN
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
id uuid,
product_id uuid,
price numeric,
quantity numeric,
fees numeric,
discount numeric,
taxable_amount numeric,
sgst_amount numeric,
cgst_amount numeric,
igst_amount numeric,
total_amount numeric,
serial_number integer
)
LOOP
INSERT INTO public.draft_invoice_details (
id,
invoice_header_id,
product_id,
price,
quantity,
fees,
discount,
taxable_amount,
sgst_amount,
cgst_amount,
igst_amount,
total_amount,
serial_number,
is_deleted
) VALUES (
detail.id,
p_invoice_id,
detail.product_id,
detail.price,
detail.quantity,
detail.fees,
detail.discount,
detail.taxable_amount,
detail.sgst_amount,
detail.cgst_amount,
detail.igst_amount,
detail.total_amount,
detail.serial_number,
false -- New records are active (not deleted)
)
ON CONFLICT (id)
DO UPDATE
SET
product_id = EXCLUDED.product_id,
price = EXCLUDED.price,
quantity = EXCLUDED.quantity,
fees = EXCLUDED.fees,
discount = EXCLUDED.discount,
taxable_amount = EXCLUDED.taxable_amount,
sgst_amount = EXCLUDED.sgst_amount,
cgst_amount = EXCLUDED.cgst_amount,
igst_amount = EXCLUDED.igst_amount,
total_amount = EXCLUDED.total_amount,
serial_number = EXCLUDED.serial_number,
is_deleted = false; -- Mark as active
END LOOP;
ELSE
-- Raise an error if the invoice cannot be edited
RAISE EXCEPTION 'Invoice cannot be edited, status is greater than or equal to 3';
END IF;
END;
$procedure$
-- Procedure: handle_invoice_update
CREATE OR REPLACE PROCEDURE public.handle_invoice_update(IN p_invoice_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_payment_term integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_round_off numeric, IN p_note text, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_modified_by uuid, IN p_invoice_details jsonb, IN p_is_draft boolean)
LANGUAGE plpgsql
AS $procedure$
DECLARE
detail RECORD; -- Record variable for each detail from the JSONB array
BEGIN
-- Check if it's a draft invoice (p_is_draft is true)
IF p_is_draft THEN
-- If it's a draft, update draft_invoice_headers and draft_invoice_details tables
UPDATE public.draft_invoice_headers
SET
company_id = p_company_id,
customer_id = p_customer_id,
currency_id = p_currency_id,
invoice_date = p_invoice_date,
payment_term = p_payment_term,
due_date = p_due_date,
total_amount = p_total_amount,
taxable_amount = p_taxable_amount,
discount = p_discount,
fees = p_fees,
sgst_amount = p_sgst_amount,
cgst_amount = p_cgst_amount,
igst_amount = p_igst_amount,
round_off = p_round_off,
note = p_note,
source_warehouse_id = p_source_warehouse_id,
destination_warehouse_id = p_destination_warehouse_id,
modified_on_utc = now(),
modified_by = p_modified_by
WHERE id = p_invoice_id;
-- Soft delete existing draft invoice details
UPDATE public.draft_invoice_details
SET is_deleted = true, deleted_on_utc = now()
WHERE invoice_header_id = p_invoice_id;
-- Insert or update new draft invoice details
FOR detail IN
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
id uuid,
product_id uuid,
price numeric,
quantity numeric,
fees numeric,
discount numeric,
taxable_amount numeric,
sgst_amount numeric,
cgst_amount numeric,
igst_amount numeric,
total_amount numeric,
serial_number integer
)
LOOP
INSERT INTO public.draft_invoice_details (
id,
invoice_header_id,
product_id,
price,
quantity,
fees,
discount,
taxable_amount,
sgst_amount,
cgst_amount,
igst_amount,
total_amount,
serial_number,
is_deleted
) VALUES (
detail.id,
p_invoice_id,
detail.product_id,
detail.price,
detail.quantity,
detail.fees,
detail.discount,
detail.taxable_amount,
detail.sgst_amount,
detail.cgst_amount,
detail.igst_amount,
detail.total_amount,
detail.serial_number,
false -- New records are active (not deleted)
)
ON CONFLICT (id)
DO UPDATE
SET
product_id = EXCLUDED.product_id,
price = EXCLUDED.price,
quantity = EXCLUDED.quantity,
fees = EXCLUDED.fees,
discount = EXCLUDED.discount,
taxable_amount = EXCLUDED.taxable_amount,
sgst_amount = EXCLUDED.sgst_amount,
cgst_amount = EXCLUDED.cgst_amount,
igst_amount = EXCLUDED.igst_amount,
total_amount = EXCLUDED.total_amount,
serial_number = EXCLUDED.serial_number,
is_deleted = false; -- Mark as active
END LOOP;
ELSE
-- If it's not a draft (p_is_draft is false), update final invoice tables
UPDATE public.invoice_headers
SET
company_id = p_company_id,
customer_id = p_customer_id,
currency_id = p_currency_id,
invoice_date = p_invoice_date,
payment_term = p_payment_term,
due_date = p_due_date,
total_amount = p_total_amount,
taxable_amount = p_taxable_amount,
discount = p_discount,
fees = p_fees,
sgst_amount = p_sgst_amount,
cgst_amount = p_cgst_amount,
igst_amount = p_igst_amount,
round_off = p_round_off,
note = p_note,
source_warehouse_id = p_source_warehouse_id,
destination_warehouse_id = p_destination_warehouse_id,
modified_on_utc = now(),
modified_by = p_modified_by
WHERE id = p_invoice_id;
-- Soft delete existing final invoice details
UPDATE public.invoice_details
SET is_deleted = true, deleted_on_utc = now()
WHERE invoice_header_id = p_invoice_id;
-- Insert or update new final invoice details
FOR detail IN
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
id uuid,
product_id uuid,
price numeric,
quantity numeric,
fees numeric,
discount numeric,
taxable_amount numeric,
sgst_amount numeric,
cgst_amount numeric,
igst_amount numeric,
total_amount numeric,
serial_number integer
)
LOOP
INSERT INTO public.invoice_details (
id,
invoice_header_id,
product_id,
price,
quantity,
fees,
discount,
taxable_amount,
sgst_amount,
cgst_amount,
igst_amount,
total_amount,
serial_number,
is_deleted
) VALUES (
detail.id,
p_invoice_id,
detail.product_id,
detail.price,
detail.quantity,
detail.fees,
detail.discount,
detail.taxable_amount,
detail.sgst_amount,
detail.cgst_amount,
detail.igst_amount,
detail.total_amount,
detail.serial_number,
false -- New records are active (not deleted)
)
ON CONFLICT (id)
DO UPDATE
SET
product_id = EXCLUDED.product_id,
price = EXCLUDED.price,
quantity = EXCLUDED.quantity,
fees = EXCLUDED.fees,
discount = EXCLUDED.discount,
taxable_amount = EXCLUDED.taxable_amount,
sgst_amount = EXCLUDED.sgst_amount,
cgst_amount = EXCLUDED.cgst_amount,
igst_amount = EXCLUDED.igst_amount,
total_amount = EXCLUDED.total_amount,
serial_number = EXCLUDED.serial_number,
is_deleted = false; -- Mark as active
END LOOP;
END IF;
END;
$procedure$
-- Procedure: initialize_draft_invoice_header_ids
CREATE OR REPLACE PROCEDURE public.initialize_draft_invoice_header_ids(IN p_default_company_id uuid, IN p_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_header_exists BOOLEAN;
v_max_id BIGINT;
v_last_seq_value BIGINT;
BEGIN
-- Check if draft invoice header IDs already exist for the new company
SELECT EXISTS (
SELECT 1
FROM draft_invoice_header_ids
WHERE company_id = p_company_id
) INTO v_header_exists;
IF NOT v_header_exists THEN
-- Get the current maximum id from the draft_invoice_header_ids table
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM draft_invoice_header_ids;
-- Get the last value generated by the draft_invoice_header_ids_id_seq sequence
SELECT last_value INTO v_last_seq_value FROM draft_invoice_header_ids_id_seq;
-- Check if the sequence needs to be advanced
IF v_last_seq_value <= v_max_id THEN
-- Advance the sequence to be one greater than the maximum id
PERFORM setval('draft_invoice_header_ids_id_seq', v_max_id + 1, false);
END IF;
-- Insert new records for the new company
INSERT INTO draft_invoice_header_ids (
id,
company_id,
fin_year,
invoice_prefix,
invoice_length,
last_invoice_id
)
SELECT
nextval('draft_invoice_header_ids_id_seq'),
p_company_id,
fin_year,
invoice_prefix,
invoice_length,
0
FROM draft_invoice_header_ids
WHERE company_id = p_default_company_id;
-- Optional: Log the operation
RAISE NOTICE 'Draft invoice header IDs initialized for company % from company %.', p_company_id, p_default_company_id;
ELSE
RAISE NOTICE 'Draft invoice header IDs already exist for company %. Skipping initialization.', p_company_id;
END IF;
END;
$procedure$
-- Procedure: initialize_invoice_payment_header_ids
CREATE OR REPLACE PROCEDURE public.initialize_invoice_payment_header_ids(IN old_company_id uuid, IN new_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_header_exists BOOLEAN;
v_max_id BIGINT;
v_last_seq_value BIGINT;
BEGIN
-- Check if invoice payment header IDs already exist for the new company
SELECT EXISTS (
SELECT 1
FROM invoice_payment_header_ids
WHERE company_id = new_company_id
) INTO v_header_exists;
IF NOT v_header_exists THEN
-- Get the current maximum id from the invoice_payment_header_ids table
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM invoice_payment_header_ids;
-- Get the last value generated by the invoice_payment_header_ids_id_seq sequence
SELECT last_value INTO v_last_seq_value FROM invoice_payment_header_ids_id_seq;
-- Check if the sequence needs to be advanced
IF v_last_seq_value <= v_max_id THEN
-- Advance the sequence to be one greater than the maximum id
PERFORM setval('invoice_payment_header_ids_id_seq', v_max_id + 1, false);
RAISE NOTICE 'Sequence invoice_payment_header_ids_id_seq advanced to: %', v_max_id + 1;
END IF;
-- Insert new records for the new company
INSERT INTO invoice_payment_header_ids (
id,
company_id,
fin_year,
payment_prefix,
payment_length,
last_payment_id
)
SELECT
nextval('invoice_payment_header_ids_id_seq'), -- Use a sequence for generating an integer ID
new_company_id, -- Assign the new company ID
fin_year, -- Copy financial year
payment_prefix, -- Copy payment prefix
payment_length, -- Copy payment length
0 -- Copy last payment ID
FROM invoice_payment_header_ids
WHERE company_id = old_company_id;
-- Optional: Log the operation
RAISE NOTICE 'Invoice payment header IDs initialized successfully for new company ID: %', new_company_id;
ELSE
RAISE NOTICE 'Invoice payment header IDs already exist for company %. Skipping initialization.', new_company_id;
END IF;
END;
$procedure$
-- Procedure: initialize_organization
CREATE OR REPLACE PROCEDURE public.initialize_organization(IN p_id uuid, IN p_name text, IN p_company_ids 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 text, IN p_email text, IN p_created_by uuid, IN p_default_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_company_id uuid; -- Variable for iterating through company IDs
v_company_name text; -- Variable for iterating through company names
v_company_ids uuid[]; -- Array to hold parsed company IDs
v_company_names text[]; -- Array to hold parsed company names
i integer; -- Iterator for looping
v_organization_exists boolean;
v_user_exists boolean;
BEGIN
-- Check if organization already exists by ID
SELECT EXISTS (
SELECT 1 FROM public.organizations WHERE id = p_id OR (id = p_id AND name = p_name)
) INTO v_organization_exists;
IF v_organization_exists THEN
RAISE NOTICE 'Organization with ID % already exists. Skipping initialization.', p_id;
ELSE
-- Insert into organizations table
INSERT INTO public.organizations (
id,
name,
created_on_utc,
created_by
) VALUES (
p_id, -- Organization ID
p_name, -- Organization Name
NOW(), -- Current UTC timestamp
p_created_by -- Created by user
);
RAISE NOTICE 'Initialized organization: % with ID: %', p_name, p_id;
END IF;
-- Parse company IDs and names
v_company_ids := string_to_array(p_company_ids, ',');
v_company_names := string_to_array(p_company_names, ',');
-- Loop through each company and initialize
FOR i IN 1..array_length(v_company_ids, 1) LOOP
v_company_id := v_company_ids[i];
v_company_name := v_company_names[i];
-- Call initialize_company for each company
CALL public.initialize_company(
v_company_id, -- Company ID passed as parameter
p_id, -- Organization ID
v_company_name, -- Organization Name
true, -- Is Apartment
p_created_by, -- Created by user
p_default_company_id -- Old Company ID passed as parameter
);
END LOOP;
v_company_id := v_company_ids[1];
-- Check if user already exists by ID or Email before creating
SELECT EXISTS (
SELECT 1 FROM public.users WHERE id = p_user_id OR email = p_email
) INTO v_user_exists;
IF NOT v_user_exists THEN
-- Call create_user function to set up the user if they don't exist
PERFORM public.create_user(
p_user_id,
p_email,
p_phone_number,
p_user_first_name,
p_user_last_name,
p_created_by,
v_company_id
);
RAISE NOTICE 'Initialized user with ID: % and email: % in organization: %', p_user_id, p_email, p_name;
ELSE
UPDATE public.users
SET company_id = v_company_id
WHERE id = p_user_id OR email = p_email;
RAISE NOTICE 'User with ID % or email % already exists. Skipping user creation.', p_user_id, p_email;
END IF;
END;
$procedure$
-- Procedure: update_bill_next_status_for_bill_header
CREATE OR REPLACE PROCEDURE public.update_bill_next_status_for_bill_header(IN p_company_id uuid, IN p_bill_ids uuid[], IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_bill RECORD;
v_account_id uuid;
v_next_status integer;
v_next_status_name character varying;
v_issue TEXT;
v_next_temp_id int;
BEGIN
RAISE NOTICE 'START update_bill_next_status_for_bill_header: company_id=%, bill_ids=%, modified_by=%', p_company_id, p_bill_ids, p_modified_by;
FOR v_bill IN
SELECT id, bill_status_id, debit_account_id
FROM public.bill_headers
WHERE id = ANY(p_bill_ids)
LOOP
RAISE NOTICE 'Processing Bill ID: %', v_bill.id;
v_account_id := v_bill.debit_account_id;
-- Get the next status from workflow
RAISE NOTICE 'Fetching next status for bill ID: %, company_id: %, current status: %', v_bill.id, p_company_id, v_bill.bill_status_id;
SELECT next_status INTO v_next_status
FROM public.bill_workflow
WHERE company_id = p_company_id
AND status = v_bill.bill_status_id
AND is_deleted = false
AND is_enabled = true
LIMIT 1;
IF v_next_status IS NULL THEN
v_issue := 'Next status not found for company and status';
RAISE NOTICE 'Next status not found for company % and status %', p_company_id, v_bill.bill_status_id;
INSERT INTO public.bill_approval_issue_logs(bill_id, issue, created_on_utc, created_by)
VALUES (v_bill.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_bill_next_status;
INSERT INTO temp_bill_next_status(id, bill_id, status, error)
VALUES (v_next_temp_id, v_bill.id, 'Not found', v_issue);
CONTINUE;
END IF;
RAISE NOTICE 'Next status for Bill ID %: %', v_bill.id, v_next_status;
-- Call `check_bill_approval_permissions` for permission check (NEW ✅)
PERFORM public.check_bill_approval_permissions(
p_company_id,
v_next_status,
p_modified_by,
v_account_id,
0
);
IF NOT FOUND THEN
v_issue := 'User is not authorized to move bill to next status';
RAISE NOTICE 'User % is not authorized to move Bill ID % to next status %', p_modified_by, v_bill.id, v_next_status;
INSERT INTO public.bill_approval_issue_logs(bill_id, issue, created_on_utc, created_by)
VALUES (v_bill.id, v_issue, now(), p_modified_by);
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_bill_next_status;
INSERT INTO temp_bill_next_status(id, bill_id, status, error)
VALUES (v_next_temp_id, v_bill.id, 'User not authorized', v_issue);
CONTINUE;
END IF;
-- Move to next status
RAISE NOTICE 'Moving Bill ID % to next status: %', v_bill.id, v_next_status;
UPDATE public.bill_headers
SET bill_status_id = v_next_status,
modified_by = p_modified_by,
modified_on_utc = now()
WHERE id = v_bill.id;
INSERT INTO public.bill_approval_logs(
bill_id, status_id, approved_by, approved_on, "comment",
created_on_utc, created_by, approval_level
)
VALUES (
v_bill.id, v_bill.bill_status_id, p_modified_by, now(),
'Approved and moved to next status',
now(), p_modified_by, 0
);
-- Get next status name for display
SELECT name INTO v_next_status_name
FROM public.bill_statuses
WHERE id = v_next_status;
SELECT COALESCE(MAX(id), 0) + 1 INTO v_next_temp_id FROM temp_bill_next_status;
INSERT INTO temp_bill_next_status(id, bill_id, status, error)
VALUES (v_next_temp_id, v_bill.id, v_next_status_name, NULL);
END LOOP;
RAISE NOTICE 'END update_bill_next_status_for_bill_header';
END
$procedure$
-- Procedure: update_invoice_account_approval_user
CREATE OR REPLACE PROCEDURE public.update_invoice_account_approval_user(IN p_updates jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
update_item jsonb;
record_exists int;
v_company_id uuid;
v_account_id uuid;
v_status_id int;
v_user_id uuid;
v_modified_by uuid;
v_approval_level int;
v_company_level int;
BEGIN
-- Loop over each item in the JSON array
FOR update_item IN SELECT * FROM jsonb_array_elements(p_updates)
LOOP
-- Extract values
v_company_id := (update_item->>'CompanyId')::uuid;
v_account_id := (update_item->>'AccountId')::uuid;
v_status_id := (update_item->>'StatusId')::int;
v_user_id := (update_item->>'UserId')::uuid;
v_modified_by := (update_item->>'ModifiedBy')::uuid;
v_approval_level := COALESCE((update_item->>'ApprovalLevel')::int, 0);
-- Check if record exists
SELECT COUNT(*) INTO record_exists
FROM public.invoice_approval_users_account
WHERE id = (update_item->>'Id')::int;
IF record_exists > 0 THEN
-- Update existing record
UPDATE public.invoice_approval_users_account
SET
company_id = v_company_id,
account_id = v_account_id,
status_id = v_status_id,
user_id = v_user_id,
approval_level = v_approval_level,
modified_by = v_modified_by,
modified_on_utc = now()
WHERE
id = (update_item->>'Id')::int
AND is_deleted = false;
ELSE
-- Insert new record
INSERT INTO public.invoice_approval_users_account (
company_id, account_id, status_id, user_id,
approval_level, created_by, created_on_utc,
modified_by, modified_on_utc, is_deleted
) VALUES (
v_company_id, v_account_id, v_status_id, v_user_id,
v_approval_level, v_modified_by, now(),
v_modified_by, now(), false
);
END IF;
-- Get company-level approval level
SELECT iw.approval_level INTO v_company_level
FROM public.invoice_workflow iw
WHERE iw.company_id = v_company_id
AND iw.status = v_status_id
AND iw.is_deleted = false
LIMIT 1;
-- If different, upsert into invoice_account_approval_levels
IF v_company_level IS DISTINCT FROM v_approval_level THEN
IF EXISTS (
SELECT 1 FROM public.invoice_account_approval_levels
WHERE company_id = v_company_id
AND account_id = v_account_id
AND status_id = v_status_id
AND is_deleted = false
) THEN
UPDATE public.invoice_account_approval_levels
SET approval_level_required = v_approval_level,
modified_on_utc = now(),
modified_by = v_modified_by
WHERE company_id = v_company_id
AND account_id = v_account_id
AND status_id = v_status_id
AND is_deleted = false;
ELSE
INSERT INTO public.invoice_account_approval_levels (
company_id, account_id, status_id, approval_level_required,
created_on_utc, modified_on_utc, deleted_on_utc,
is_deleted, created_by, modified_by
) VALUES (
v_company_id, v_account_id, v_status_id, v_approval_level,
now(), NULL, NULL,
false, v_modified_by, v_modified_by
);
END IF;
END IF;
END LOOP;
END;
$procedure$
-- Procedure: update_invoice_header
CREATE OR REPLACE PROCEDURE public.update_invoice_header(IN p_invoice_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_customer_account_id uuid, IN p_sales_account_id uuid, IN p_invoice_date date, IN p_due_date date, IN p_payment_term integer, IN p_taxable_amount numeric, IN p_cgst_amount numeric, IN p_sgst_amount numeric, IN p_igst_amount numeric, IN p_total_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_round_off numeric, IN p_currency_id integer, IN p_note character varying, IN p_invoice_status_id integer, IN p_modified_by uuid, IN p_type integer)
LANGUAGE plpgsql
AS $procedure$
BEGIN
UPDATE public.invoice_headers
SET
company_id = p_company_id,
customer_id = p_customer_id,
debit_account_id = p_customer_account_id,
credit_account_id = p_sales_account_id,
invoice_date = p_invoice_date,
due_date = p_due_date,
payment_term = p_payment_term,
taxable_amount = p_taxable_amount,
cgst_amount = p_cgst_amount,
sgst_amount = p_sgst_amount,
igst_amount = p_igst_amount,
total_amount = p_total_amount,
discount = p_discount,
fees = p_fees,
round_off = p_round_off,
currency_id = p_currency_id,
note = p_note,
invoice_status_id = p_invoice_status_id,
modified_by = p_modified_by,
modified_on_utc = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
type = p_type
WHERE id = p_invoice_id;
END;
$procedure$
-- Procedure: via_excel_grouped_invoices
CREATE OR REPLACE PROCEDURE public.via_excel_grouped_invoices(IN p_group_invoice_batch_id text, IN p_grouped_invoice_template_id uuid, IN p_execution_date date, IN p_total_count integer, IN p_success_count integer, IN p_company_id uuid, IN p_invoice_unit_data jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_group_invoice_header_id UUID := gen_random_uuid();
v_group_invoice_number TEXT;
v_created_by UUID;
v_invoice_header_id UUID;
v_unit_id INTEGER;
v_reference_number TEXT;
v_invoice_data JSONB;
v_is_duplicate BOOLEAN;
v_is_detail_duplicate BOOLEAN;
v_success_count INTEGER;
BEGIN
-- Fetch the system user ID from the users table
SELECT id INTO v_created_by
FROM users
WHERE first_name = 'System'
LIMIT 1;
IF v_created_by IS NULL THEN
RAISE EXCEPTION 'System user not found in the users table';
END IF;
-- Generate new group invoice number
v_group_invoice_number := get_new_group_invoice_number(p_company_id, p_execution_date);
-- Check if the batch ID already exists in group_invoice_headers
SELECT EXISTS (
SELECT 1
FROM public.group_invoice_headers
WHERE group_invoice_template_id = p_grouped_invoice_template_id
AND company_id = p_company_id
AND group_invoice_batch_id = p_group_invoice_batch_id::text
) INTO v_is_duplicate;
-- Insert into group_invoice_headers if not duplicate
IF NOT v_is_duplicate THEN
INSERT INTO public.group_invoice_headers(
id,
company_id,
group_invoice_template_id,
group_invoice_number,
group_invoice_batch_id,
execution_date,
error_message,
total_count,
success_count,
created_by,
created_on_utc
)
VALUES (
v_group_invoice_header_id,
p_company_id,
p_grouped_invoice_template_id,
v_group_invoice_number,
p_group_invoice_batch_id,
p_execution_date,
'',
p_total_count,
p_success_count,
v_created_by,
NOW()
);
ELSE
SELECT id
INTO v_group_invoice_header_id
FROM public.group_invoice_headers
WHERE company_id = p_company_id
AND group_invoice_batch_id = p_group_invoice_batch_id::text
LIMIT 1;
END IF;
-- Loop through the JSONB data
FOR v_invoice_data IN SELECT * FROM jsonb_array_elements(p_invoice_unit_data)
LOOP
-- Extract values from JSONB
v_unit_id := (v_invoice_data->>'unit_id')::INTEGER;
v_reference_number := v_invoice_data->>'reference_number';
-- Fetch invoice_header_id based on invoice_number
SELECT id INTO v_invoice_header_id
FROM public.invoice_headers
WHERE invoice_number = (v_invoice_data->>'invoice_number')
AND company_id = p_company_id;
IF v_invoice_header_id IS NOT NULL THEN
-- Check if group_invoice_details already exists
SELECT EXISTS (
SELECT 1
FROM public.group_invoice_details
WHERE group_invoice_header_id = v_group_invoice_header_id
AND invoice_header_id = v_invoice_header_id
AND company_id = p_company_id
AND unit_id = v_unit_id
) INTO v_is_detail_duplicate;
IF NOT v_is_detail_duplicate THEN
-- Insert into group_invoice_details
INSERT INTO public.group_invoice_details(
id,
group_invoice_header_id,
invoice_header_id,
posted_on,
status,
error_message,
created_on_utc,
created_by,
unit_id,
draft_invoice_header_id,
company_id,
reference_number
)
VALUES (
gen_random_uuid(),
v_group_invoice_header_id,
v_invoice_header_id,
p_execution_date,
'success',
'',
NOW(),
v_created_by,
v_unit_id,
'00000000-0000-0000-0000-000000000000',
p_company_id,
v_reference_number
);
END IF;
END IF;
END LOOP;
-- Count successful details for this group invoice header
SELECT COUNT(*)
INTO v_success_count
FROM public.group_invoice_details
WHERE group_invoice_header_id = v_group_invoice_header_id
AND status = 'success';
-- Update the success_count in group_invoice_headers
UPDATE public.group_invoice_headers
SET success_count = v_success_count
WHERE id = v_group_invoice_header_id;
END;
$procedure$
-- Procedure: purge_sales_organization_data
CREATE OR REPLACE PROCEDURE public.purge_sales_organization_data(IN p_organization_ids uuid[] DEFAULT NULL::uuid[])
LANGUAGE plpgsql
AS $procedure$
DECLARE
c_retention_hours integer := 24;
v_cutoff_24h timestamp := now() - make_interval(hours => GREATEST(c_retention_hours, 1));
v_orgs uuid[] := '{}';
v_companies uuid[] := '{}';
v_customer_ids uuid[] := '{}';
v_invoice_header_ids uuid[] := '{}';
v_invoice_detail_ids uuid[] := '{}';
v_invoice_payment_header_ids uuid[] := '{}';
v_invoice_payment_detail_ids uuid[] := '{}';
v_group_invoice_header_ids uuid[] := '{}';
v_group_invoice_detail_ids uuid[] := '{}';
v_draft_invoice_header_ids uuid[] := '{}';
v_draft_invoice_detail_ids uuid[] := '{}';
v_customer_note_header_ids uuid[] := '{}';
v_customer_note_detail_ids uuid[] := '{}';
v_gate_pass_header_ids uuid[] := '{}';
v_gate_pass_detail_ids uuid[] := '{}';
v_invoice_workflow_ids uuid[] := '{}';
v_invoice_voucher_ids uuid[] := '{}';
v_penalty_config_ids uuid[] := '{}';
v_recurring_sales_schedule_ids uuid[] := '{}';
v_group_invoice_template_ids uuid[] := '{}';
BEGIN
-- 1) Resolve target organizations and companies
IF p_organization_ids IS NULL OR array_length(p_organization_ids, 1) IS NULL THEN
SELECT COALESCE(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 sales cleanup.';
RETURN;
END IF;
SELECT COALESCE(array_agg(id), '{}')
INTO v_companies
FROM companies
WHERE organization_id = ANY(v_orgs);
IF v_companies IS NULL OR array_length(v_companies, 1) IS NULL THEN
RAISE NOTICE 'No companies resolved for sales purge. Orgs: %', v_orgs;
RETURN;
END IF;
RAISE NOTICE 'Sales purge targets - Organizations: %; Companies: %', v_orgs, v_companies;
-- 2) Collect IDs for child→parent deletion order
SELECT COALESCE(array_agg(id), '{}')
INTO v_customer_ids
FROM customers
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_invoice_header_ids
FROM invoice_headers
WHERE company_id = ANY(v_companies)
AND created_on_utc < v_cutoff_24h;
SELECT COALESCE(array_agg(id), '{}')
INTO v_invoice_detail_ids
FROM invoice_details
WHERE invoice_header_id = ANY(v_invoice_header_ids);
SELECT COALESCE(array_agg(id), '{}')
INTO v_invoice_payment_header_ids
FROM invoice_payment_headers
WHERE company_id = ANY(v_companies)
AND created_on_utc < v_cutoff_24h;
SELECT COALESCE(array_agg(id), '{}')
INTO v_invoice_payment_detail_ids
FROM invoice_payment_details
WHERE invoice_payment_header_id = ANY(v_invoice_payment_header_ids);
SELECT COALESCE(array_agg(id), '{}')
INTO v_group_invoice_header_ids
FROM group_invoice_headers
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_group_invoice_detail_ids
FROM group_invoice_details
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_draft_invoice_header_ids
FROM draft_invoice_headers
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_draft_invoice_detail_ids
FROM draft_invoice_details
WHERE invoice_header_id = ANY(v_draft_invoice_header_ids);
SELECT COALESCE(array_agg(id), '{}')
INTO v_customer_note_header_ids
FROM customer_note_headers
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_customer_note_detail_ids
FROM customer_note_details
WHERE customer_note_header_id = ANY(v_customer_note_header_ids);
SELECT COALESCE(array_agg(id), '{}')
INTO v_gate_pass_header_ids
FROM gate_pass_headers
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_gate_pass_detail_ids
FROM gate_pass_details
WHERE gate_pass_header_id = ANY(v_gate_pass_header_ids);
SELECT COALESCE(array_agg(id), '{}')
INTO v_invoice_workflow_ids
FROM invoice_workflow
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_invoice_voucher_ids
FROM invoice_voucher_ids
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_penalty_config_ids
FROM penalty_configs
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_recurring_sales_schedule_ids
FROM recurring_sales_schedules
WHERE company_id = ANY(v_companies);
SELECT COALESCE(array_agg(id), '{}')
INTO v_group_invoice_template_ids
FROM group_invoice_templates
WHERE company_id = ANY(v_companies);
-- 3) Purge in strict child → parent order (avoid FK violations)
DELETE FROM customer_note_details
WHERE customer_note_header_id = ANY(v_customer_note_header_ids);
DELETE FROM customer_note_headers
WHERE id = ANY(v_customer_note_header_ids);
DELETE FROM customer_note_work_flows
WHERE company_id = ANY(v_companies);
DELETE FROM group_invoice_details
WHERE company_id = ANY(v_companies);
DELETE FROM group_invoice_headers
WHERE id = ANY(v_group_invoice_header_ids);
DELETE FROM group_invoice_header_ids
WHERE company_id = ANY(v_companies);
DELETE FROM group_invoice_templates
WHERE id = ANY(v_group_invoice_template_ids);
DELETE FROM invoice_payment_details
WHERE invoice_payment_header_id = ANY(v_invoice_payment_header_ids);
DELETE FROM invoice_payment_headers
WHERE id = ANY(v_invoice_payment_header_ids);
DELETE FROM invoice_payment_header_ids
WHERE company_id = ANY(v_companies);
DELETE FROM invoice_details
WHERE invoice_header_id = ANY(v_invoice_header_ids);
DELETE FROM invoice_headers
WHERE id = ANY(v_invoice_header_ids);
DELETE FROM invoice_header_ids
WHERE company_id = ANY(v_companies);
DELETE FROM invoice_workflow
WHERE id = ANY(v_invoice_workflow_ids);
DELETE FROM invoice_voucher_ids
WHERE id = ANY(v_invoice_voucher_ids);
DELETE FROM draft_invoice_details
WHERE invoice_header_id = ANY(v_draft_invoice_header_ids);
DELETE FROM draft_invoice_headers
WHERE id = ANY(v_draft_invoice_header_ids);
DELETE FROM draft_invoice_header_ids
WHERE company_id = ANY(v_companies);
DELETE FROM gate_pass_details
WHERE gate_pass_header_id = ANY(v_gate_pass_header_ids);
DELETE FROM gate_pass_headers
WHERE id = ANY(v_gate_pass_header_ids);
DELETE FROM penalty_configs
WHERE id = ANY(v_penalty_config_ids);
DELETE FROM recurring_sales_schedules
WHERE id = ANY(v_recurring_sales_schedule_ids);
DELETE FROM customers
WHERE id = ANY(v_customer_ids);
DELETE FROM users
WHERE company_id = ANY(v_companies);
DELETE FROM companies
WHERE id = ANY(v_companies);
RAISE NOTICE 'Sales purge complete for companies: % (orgs: %).', v_companies, v_orgs;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'purge_sales_organization_data failed: %', SQLERRM;
END;
$procedure$
-- Procedure: close_resolved_delinquency_cycles
CREATE OR REPLACE PROCEDURE public.close_resolved_delinquency_cycles(IN p_organization_id uuid, IN p_company_ids uuid[], IN p_modified_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
CYCLE_STATUS_CLOSE CONSTANT int := 2;
BEGIN
UPDATE delinquency_cycles dc
SET
ended_on_utc = CURRENT_TIMESTAMP,
status_id = CYCLE_STATUS_CLOSE, -- CLOSED
modified_on_utc = CURRENT_TIMESTAMP,
modified_by = p_modified_by
WHERE dc.organization_id = p_organization_id
AND dc.ended_on_utc IS NULL
AND dc.is_deleted = false
AND NOT EXISTS (
SELECT 1
FROM invoice_headers ih
WHERE ih.company_id = ANY (p_company_ids)
AND ih.customer_id = dc.customer_id
AND ih.is_deleted = false
AND (ih.total_amount - ih.settled_amount) > 0
);
END;
$procedure$
-- Procedure: schedule_invoice
CREATE OR REPLACE PROCEDURE public.schedule_invoice()
LANGUAGE plpgsql
AS $procedure$
DECLARE
templateRecord RECORD;
executionDate DATE;
nextExecutionDate DATE;
newDraftInvoiceId UUID;
invoiceHeaderRecord RECORD;
invoiceDetailRecord RECORD;
BEGIN
-- Loop through all active templates in the invoice_template table
FOR templateRecord IN
SELECT *
FROM public.invoice_template
WHERE is_deleted = false
AND starts_from <= CURRENT_DATE
AND end_date >= CURRENT_DATE
LOOP
-- Initialize executionDate to the start date of the template
executionDate := templateRecord.starts_from;
-- Fetch data from invoice_headers or draft_invoice_headers based on invoiceHeaderId
SELECT * INTO invoiceHeaderRecord
FROM public.invoice_headers
WHERE id = templateRecord.invoice_header_id AND is_deleted = false;
IF NOT FOUND THEN
-- If not found in invoice_headers, check in draft_invoice_headers
SELECT * INTO invoiceHeaderRecord
FROM public.draft_invoice_headers
WHERE id = templateRecord.invoice_header_id AND is_deleted = false;
END IF;
-- Loop through the date range to create execution dates
WHILE executionDate <= templateRecord.end_date LOOP
-- Calculate the next execution date based on frequency
CASE templateRecord.frequency_cycle
WHEN 'daily' THEN
nextExecutionDate := executionDate + INTERVAL '1 day';
WHEN 'weekly' THEN
nextExecutionDate := executionDate + INTERVAL '1 week';
WHEN 'monthly' THEN
nextExecutionDate := executionDate + INTERVAL '1 month';
WHEN 'yearly' THEN
nextExecutionDate := executionDate + INTERVAL '1 year';
ELSE
RAISE EXCEPTION 'Invalid frequency cycle: %', templateRecord.frequency_cycle;
END CASE;
-- Create a new draft invoice
newDraftInvoiceId := gen_random_uuid();
CALL public.create_draft_invoice_header(
newDraftInvoiceId,
invoiceHeaderRecord.company_id,
invoiceHeaderRecord.customer_id,
invoiceHeaderRecord.debit_account_id,
invoiceHeaderRecord.credit_account_id,
executionDate,
executionDate + INTERVAL '30 days', -- Set due date
invoiceHeaderRecord.payment_term,
invoiceHeaderRecord.taxable_amount,
invoiceHeaderRecord.cgst_amount,
invoiceHeaderRecord.sgst_amount,
invoiceHeaderRecord.igst_amount,
invoiceHeaderRecord.total_amount,
invoiceHeaderRecord.discount,
invoiceHeaderRecord.fees,
invoiceHeaderRecord.round_off,
invoiceHeaderRecord.currency_id,
invoiceHeaderRecord.note,
1, -- Set initial status to 'draft'
invoiceHeaderRecord.created_by,
invoiceHeaderRecord.invoice_voucher,
invoiceHeaderRecord.source_warehouse_id,
invoiceHeaderRecord.destination_warehouse_id,
invoiceHeaderRecord.so_no,
invoiceHeaderRecord.so_date,
invoiceHeaderRecord.type
);
-- Insert related draft invoice details
FOR invoiceDetailRecord IN
SELECT * FROM public.invoice_details
WHERE invoice_header_id = templateRecord.invoice_header_id AND is_deleted = false
LOOP
CALL public.create_draft_invoice_detail(
newDraftInvoiceId,
invoiceDetailRecord.price,
invoiceDetailRecord.quantity,
invoiceDetailRecord.discount,
invoiceDetailRecord.fees,
invoiceDetailRecord.cgst_amount,
invoiceDetailRecord.igst_amount,
invoiceDetailRecord.sgst_amount,
invoiceDetailRecord.taxable_amount,
invoiceDetailRecord.total_amount,
invoiceDetailRecord.serial_number,
invoiceDetailRecord.product_id
);
END LOOP;
-- Insert into apartment_invoice_template_executions
INSERT INTO public.apartment_invoice_template_executions (
id, template_id, execution_date, error_message, success_count,
total_count, created_on_utc, created_by, is_deleted
)
VALUES (
gen_random_uuid(), templateRecord.id, executionDate, '', 1, 1,
NOW(), invoiceHeaderRecord.created_by, false
);
-- Update executionDate for the next iteration
executionDate := nextExecutionDate;
END LOOP;
END LOOP;
RAISE NOTICE 'Invoice scheduling completed successfully.';
END;
$procedure$
-- Procedure: test_run_batches
CREATE OR REPLACE PROCEDURE public.test_run_batches()
LANGUAGE plpgsql
AS $procedure$
DECLARE
schedule_record RECORD;
v_unit RECORD;
v_execution_id UUID;
v_invoice_header_id UUID;
v_total_amount NUMERIC;
v_status TEXT;
v_error_message TEXT;
v_total_count INTEGER;
v_success_count INTEGER;
v_calculation_type INTEGER;
v_company_id UUID;
v_sales_account_id UUID;
v_account_receivable_id UUID;
v_invoice_date DATE;
v_starts_from DATE;
v_end_date DATE;
v_due_date DATE;
v_payment_term INTEGER;
v_currency_id INTEGER;
v_fixed_product_id UUID;
v_bhk_product_id UUID;
v_sqft_product_id UUID;
v_fixed_product_rate NUMERIC;
v_bhk_product_rate NUMERIC;
v_sqft_product_rate NUMERIC;
v_note TEXT;
v_invoice_status_id INTEGER;
v_due_days INTEGER;
v_billing_cycle TEXT;
v_half_year INTEGER;
v_quarters_of_month INTEGER;
v_bi_month INTEGER;
v_day_of_week INTEGER;
v_day_of_month INTEGER;
v_month_of_year INTEGER;
v_time_of_day INTERVAL;
v_created_by UUID;
v_time_as_time TIME;
BEGIN
-- Loop through all non-deleted schedules that should run today
FOR schedule_record IN
SELECT *
FROM batch_schedules
WHERE is_deleted = FALSE
AND template_id IN (
SELECT id
FROM public.apartment_invoice_templates
WHERE is_deleted = FALSE
AND (starts_from <= CURRENT_DATE
AND (end_date IS NULL OR end_date >= CURRENT_DATE))
)
AND last_executed_at IS DISTINCT FROM CURRENT_DATE::timestamp
LOOP
-- Assign values from schedule_record
v_half_year := schedule_record.half_year;
v_quarters_of_month := schedule_record.quarters_of_month;
v_bi_month := schedule_record.bi_month;
v_day_of_week := schedule_record.day_of_week;
v_day_of_month := schedule_record.day_of_month;
v_month_of_year := schedule_record.month_of_year;
v_time_of_day := schedule_record.time_of_day;
-- Convert v_time_of_day interval to time by casting extracted values to integer
v_time_as_time := make_time(
EXTRACT(HOUR FROM v_time_of_day)::INTEGER,
EXTRACT(MINUTE FROM v_time_of_day)::INTEGER,
EXTRACT(SECOND FROM v_time_of_day)::INTEGER
);
-- Log important information before entering the IF block
RAISE NOTICE 'Checking if batch should run for template_id: %', schedule_record.template_id;
-- Process based on billing cycle and check if current time matches the set time
IF (
-- Daily schedules
schedule_record.billing_cycle = 'Daily'
OR (schedule_record.billing_cycle = 'Weekly' AND v_day_of_week = EXTRACT(DOW FROM CURRENT_DATE))
OR (schedule_record.billing_cycle = 'Monthly' AND v_day_of_month = EXTRACT(DAY FROM CURRENT_DATE) AND CURRENT_DATE <= v_end_date)
OR (schedule_record.billing_cycle = 'Bi-Monthly' AND v_bi_month = CEIL(EXTRACT(MONTH FROM CURRENT_DATE) / 2) AND CURRENT_DATE <= v_end_date)
OR (schedule_record.billing_cycle = 'Quarterly' AND v_quarters_of_month = CEIL(EXTRACT(MONTH FROM CURRENT_DATE) / 3) AND CURRENT_DATE <= v_end_date)
OR (schedule_record.billing_cycle = 'Half-yearly' AND v_half_year = CASE WHEN EXTRACT(MONTH FROM CURRENT_DATE) <= 6 THEN 1 ELSE 2 END AND CURRENT_DATE <= v_end_date)
OR (schedule_record.billing_cycle = 'Yearly' AND v_month_of_year = EXTRACT(MONTH FROM CURRENT_DATE) AND CURRENT_DATE <= v_end_date)
)
AND CURRENT_TIME >= v_time_as_time THEN
-- Log the execution details
RAISE NOTICE 'Batch should run for template_id: % at time: %', schedule_record.template_id, v_time_as_time;
-- Begin processing the batch for this template
v_execution_id := gen_random_uuid();
v_total_count := 0;
v_success_count := 0;
v_status := 'pending';
v_error_message := '';
-- Insert into apartment_invoice_template_execution
BEGIN
INSERT INTO public.apartment_invoice_template_executions (
id, template_id, execution_date, success_count, created_on_utc, created_by, total_count, error_message
)
VALUES (
v_execution_id, schedule_record.template_id, NOW(), v_success_count, NOW(), v_created_by, v_total_count, v_error_message
);
RAISE NOTICE 'Inserted into apartment_invoice_template_executions for template_id: % with execution_id: %', schedule_record.template_id, v_execution_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Error inserting into apartment_invoice_template_executions for template_id: % - Error: %', schedule_record.template_id, SQLERRM;
END;
-- Select Status from the invoice workflow
SELECT MIN(status)
INTO v_invoice_status_id
FROM public.invoice_workflow
WHERE company_id = v_company_id
AND is_deleted = FALSE;
-- Loop through units associated with the template
FOR v_unit IN
SELECT unit_id, bhk, sqft_area, customer_id
FROM public.apartment_invoice_template_units
WHERE template_id = schedule_record.template_id
LOOP
BEGIN
v_total_count := v_total_count + 1;
v_invoice_header_id := gen_random_uuid();
v_total_amount := 0;
-- Calculate the total amount based on the calculation type
CASE v_calculation_type
WHEN 1 THEN -- Fixed method
v_total_amount := v_fixed_product_rate;
WHEN 2 THEN -- BHK
v_total_amount := v_bhk_product_rate * (v_unit.bhk)::NUMERIC;
WHEN 3 THEN -- SFT
v_total_amount := v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC;
WHEN 4 THEN -- Fixed + BHK
v_total_amount := v_fixed_product_rate + (v_bhk_product_rate * (v_unit.bhk)::NUMERIC);
WHEN 5 THEN -- Fixed + SFT
v_total_amount := v_fixed_product_rate + (v_sqft_product_rate * (v_unit.sqft_area)::NUMERIC);
END CASE;
-- Call to create invoice header
CALL public.create_invoice_header(
v_invoice_header_id,
v_company_id,
v_unit.customer_id,
v_account_receivable_id,
v_sales_account_id,
v_invoice_date,
v_due_date, -- Now passing the calculated due_date
v_payment_term,
v_total_amount,
0.0,
0.0,
0.0,
v_total_amount,
0.0,
0.0,
0.0,
v_currency_id,
v_note,
v_invoice_status_id,
v_created_by,
'APINV',
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000000',
(CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), -- created_on_utc set to current UTC time
COALESCE(NULLIF('', ''), ''), -- so_no as empty string
v_invoice_date,
3, -- Apartment type
v_created_by
);
-- Log success for invoice header creation
RAISE NOTICE 'Created invoice header for unit_id: %', v_unit.unit_id;
EXCEPTION
WHEN OTHERS THEN
-- Handle any errors during invoice posting
v_status := 'failed';
v_error_message := COALESCE(SQLERRM, 'Unknown error');
RAISE NOTICE 'Error creating invoice header for unit_id: % - Error: %', v_unit.unit_id, v_error_message;
END;
END LOOP;
-- Final logging after processing template
RAISE NOTICE 'Processing complete for template_id: %', schedule_record.template_id;
ELSE
RAISE NOTICE 'Batch skipped for template_id: %', schedule_record.template_id;
END IF;
END LOOP;
END;
$procedure$
-- Procedure: create_customer_note
CREATE OR REPLACE PROCEDURE public.create_customer_note(IN p_is_debit_note boolean, IN p_customer_note_header_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_note_date date, IN p_invoice_id uuid, IN p_credit_account_id uuid, IN p_debit_account_id uuid, IN p_customer_note_status_id integer, IN p_fees numeric, IN p_discount numeric, IN p_taxable_amount numeric, IN p_cgst_amount numeric, IN p_sgst_amount numeric, IN p_igst_amount numeric, IN p_round_off numeric, IN p_total_amount numeric, IN p_note text, IN p_created_by uuid, IN p_customer_note_details jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
detail JSONB;
BEGIN
-- Insert into customer_note_headers table
INSERT INTO public.customer_note_headers (
is_debit_note,
id,
company_id,
customer_id,
note_date,
invoice_id,
credit_account_id,
debit_account_id,
customer_note_status_id,
fees,
discount,
taxable_amount,
cgst_amount,
sgst_amount,
igst_amount,
round_off,
total_amount,
note,
created_on_utc,
created_by,
is_deleted
)
VALUES (
p_is_debit_note, -- is_debit_note
p_customer_note_header_id, -- id
p_company_id, -- company_id
p_customer_id, -- customer_id
p_note_date, -- note_date
p_invoice_id, -- invoice_id
p_credit_account_id, -- credit_account_id
p_debit_account_id, -- debit_account_id
p_customer_note_status_id, -- note_status_id
p_fees, -- fees
p_discount, -- discount
p_taxable_amount, -- taxable_amount
p_cgst_amount, -- cgst_amount
p_sgst_amount, -- sgst_amount
p_igst_amount, -- igst_amount
p_round_off, -- round_off
p_total_amount, -- total_amount
p_note, -- note
now(), -- created_on_utc
p_created_by, -- created_by
false -- is_deleted
);
-- Loop through each element of the JSONB array p_customer_note_details
FOR detail IN
SELECT * FROM jsonb_array_elements(p_customer_note_details)
LOOP
INSERT INTO public.customer_note_details (
id,
customer_note_header_id,
product_id,
quantity,
price,
discount,
fees,
taxable_amount,
cgst_amount,
sgst_amount,
igst_amount,
total_amount,
is_deleted
)
VALUES (
(detail->>'id')::UUID,
p_customer_note_header_id,
(detail->>'product_id')::UUID,
(detail->>'quantity')::INTEGER,
(detail->>'price')::NUMERIC,
(detail->>'discount')::NUMERIC,
(detail->>'fees')::NUMERIC,
(detail->>'taxable_amount')::NUMERIC,
(detail->>'cgst_amount')::NUMERIC,
(detail->>'sgst_amount')::NUMERIC,
(detail->>'igst_amount')::NUMERIC,
(detail->>'total_amount')::NUMERIC,
false
);
END LOOP;
RAISE NOTICE 'Customer note and details inserted successfully';
END;
$procedure$
-- Procedure: run_sales_invoice_schedule
CREATE OR REPLACE PROCEDURE public.run_sales_invoice_schedule()
LANGUAGE plpgsql
AS $procedure$
DECLARE
templateRecord RECORD;
executionDate DATE;
nextExecutionDate DATE;
newDraftInvoiceId UUID;
invoiceHeaderRecord RECORD;
invoiceDetailRecord RECORD;
BEGIN
-- Loop through all active templates in the run_sales_invoice_schedule table
FOR templateRecord IN
SELECT *
FROM public.run_sales_invoice_schedule
WHERE is_deleted = false
AND starts_from <= CURRENT_DATE
AND end_date >= CURRENT_DATE
AND schedule_status = 'active'
LOOP
-- Initialize executionDate to the start date of the template
executionDate := templateRecord.starts_from;
-- Fetch data from invoice_headers or draft_invoice_headers based on invoiceHeaderId
SELECT * INTO invoiceHeaderRecord
FROM public.invoice_headers
WHERE id = templateRecord.invoice_header_id AND is_deleted = false;
IF NOT FOUND THEN
-- If not found in invoice_headers, check in draft_invoice_headers
SELECT * INTO invoiceHeaderRecord
FROM public.draft_invoice_headers
WHERE id = templateRecord.invoice_header_id AND is_deleted = false;
END IF;
-- Loop through the date range to create execution dates
WHILE executionDate <= templateRecord.end_date LOOP
-- Calculate the next execution date based on frequency
CASE templateRecord.frequency_cycle
WHEN 'daily' THEN
nextExecutionDate := executionDate + INTERVAL '1 day';
WHEN 'weekly' THEN
nextExecutionDate := executionDate + INTERVAL '1 week';
WHEN 'monthly' THEN
nextExecutionDate := executionDate + INTERVAL '1 month';
WHEN 'yearly' THEN
nextExecutionDate := executionDate + INTERVAL '1 year';
ELSE
RAISE EXCEPTION 'Invalid frequency cycle: %', templateRecord.frequency_cycle;
END CASE;
-- Create a new draft invoice
newDraftInvoiceId := gen_random_uuid();
CALL public.create_draft_invoice_header(
newDraftInvoiceId,
invoiceHeaderRecord.company_id,
invoiceHeaderRecord.customer_id,
invoiceHeaderRecord.debit_account_id,
invoiceHeaderRecord.credit_account_id,
executionDate,
executionDate + INTERVAL '30 days', -- Set due date
invoiceHeaderRecord.payment_term,
invoiceHeaderRecord.taxable_amount,
invoiceHeaderRecord.cgst_amount,
invoiceHeaderRecord.sgst_amount,
invoiceHeaderRecord.igst_amount,
invoiceHeaderRecord.total_amount,
invoiceHeaderRecord.discount,
invoiceHeaderRecord.fees,
invoiceHeaderRecord.round_off,
invoiceHeaderRecord.currency_id,
invoiceHeaderRecord.note,
1, -- Set initial status to 'draft'
'Generated by System', -- Set created_by to 'Generated by System'
invoiceHeaderRecord.invoice_voucher,
invoiceHeaderRecord.source_warehouse_id,
invoiceHeaderRecord.destination_warehouse_id,
invoiceHeaderRecord.so_no,
invoiceHeaderRecord.so_date,
invoiceHeaderRecord.type
);
-- Insert related draft invoice details
FOR invoiceDetailRecord IN
SELECT * FROM public.invoice_details
WHERE invoice_header_id = templateRecord.invoice_header_id AND is_deleted = false
LOOP
CALL public.create_draft_invoice_detail(
newDraftInvoiceId,
invoiceDetailRecord.price,
invoiceDetailRecord.quantity,
invoiceDetailRecord.discount,
invoiceDetailRecord.fees,
invoiceDetailRecord.cgst_amount,
invoiceDetailRecord.igst_amount,
invoiceDetailRecord.sgst_amount,
invoiceDetailRecord.taxable_amount,
invoiceDetailRecord.total_amount,
invoiceDetailRecord.serial_number,
invoiceDetailRecord.product_id
);
END LOOP;
-- Insert into apartment_invoice_template_executions
INSERT INTO public.apartment_invoice_template_executions (
id, template_id, execution_date, error_message, success_count,
total_count, created_on_utc, created_by, is_deleted
)
VALUES (
gen_random_uuid(), templateRecord.id, executionDate, '', 1, 1,
NOW(), 'Generated by System', false
);
-- Update executionDate for the next iteration
executionDate := nextExecutionDate;
END LOOP;
-- If all iterations are completed, set schedule_status to 'completed'
UPDATE public.run_sales_invoice_schedule
SET schedule_status = 'completed'
WHERE id = templateRecord.id;
END LOOP;
-- Set schedules to 'inactive' if they are not processed
UPDATE public.run_sales_invoice_schedule
SET schedule_status = 'inactive'
WHERE is_deleted = false
AND starts_from > CURRENT_DATE
AND schedule_status = 'active';
RAISE NOTICE 'Invoice scheduling completed successfully.';
END;
$procedure$
-- Procedure: create_draft_invoice_header
CREATE OR REPLACE PROCEDURE public.create_draft_invoice_header(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_invoice_date date, IN p_due_date date, IN p_payment_term integer, IN p_taxable_amount numeric, IN p_cgst_amount numeric, IN p_sgst_amount numeric, IN p_igst_amount numeric, IN p_total_amount numeric, IN p_discount numeric, IN p_fees numeric, IN p_round_off numeric, IN p_currency_id integer, IN p_note character varying, IN p_invoice_status_id integer, IN p_created_by uuid, IN p_invoice_voucher character varying, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_so_no character varying, IN p_so_date date, IN p_type integer)
LANGUAGE plpgsql
AS $procedure$
DECLARE
invoice_number character varying;
BEGIN
invoice_number := get_new_draft_invoice_number(p_company_id,p_invoice_date);
RAISE NOTICE 'Value: %', invoice_number;
INSERT INTO
public.draft_invoice_headers(
id,
company_id,
customer_id,
credit_account_id,
debit_account_id,
invoice_number,
invoice_date,
due_date,
payment_term,
taxable_amount,
cgst_amount,
sgst_amount,
igst_amount,
total_amount,
discount,
fees,
round_off,
currency_id,
note,
invoice_status_id,
created_by,
invoice_voucher,
source_warehouse_id,
destination_warehouse_id,
created_on_utc,
so_no,
so_date,
type,
current_approval_level,
payment_status_id)
VALUES (
p_id,
p_company_id,
p_customer_id,
p_debit_account_id,
p_credit_account_id,
invoice_number,
p_invoice_date,
p_due_date,
p_payment_term,
p_taxable_amount,
p_cgst_amount,
p_sgst_amount,
p_igst_amount,
p_total_amount,
p_discount,
p_fees,
p_round_off,
p_currency_id,
p_note,
p_invoice_status_id,
p_created_by,
p_invoice_voucher,
p_source_warehouse_id,
p_destination_warehouse_id,
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
p_so_no,
p_so_date,
p_type,
0,
0
);
END;
$procedure$
-- Procedure: run_scheduled_invoice
CREATE OR REPLACE PROCEDURE public.run_scheduled_invoice(IN p_invoice_header_id uuid, IN p_draft_invoice_header_id uuid, IN p_schedule_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_system_user_id uuid;
v_execution_log_id uuid := gen_random_uuid();
v_related_schedule_id uuid;
v_new_draft_invoice_id uuid;
v_source_is_live boolean := false;
v_source_invoice_id uuid;
src_header RECORD;
src_detail RECORD;
BEGIN
-- ✅ 1. Find "System" user
SELECT u.id
INTO v_system_user_id
FROM public.users AS u
WHERE u.is_deleted = false
AND lower(u.first_name) = 'system'
ORDER BY u.created_on_utc NULLS LAST, u.id
LIMIT 1;
IF v_system_user_id IS NULL THEN
RAISE NOTICE 'No user with first_name="System" found; proceeding with NULL triggered_by.';
END IF;
-- ✅ 2. Determine which ID to use as source
IF p_invoice_header_id IS NOT NULL THEN
v_source_invoice_id := p_invoice_header_id;
v_source_is_live := true;
ELSIF p_draft_invoice_header_id IS NOT NULL THEN
v_source_invoice_id := p_draft_invoice_header_id;
v_source_is_live := false;
ELSE
RAISE NOTICE 'Both invoice_header_id and draft_invoice_header_id are NULL; aborting.';
RETURN;
END IF;
-- ✅ 3. Get related schedule_id
SELECT s.id
INTO v_related_schedule_id
FROM public.recurring_sales_schedules AS s
WHERE s.is_deleted = false
AND (
(v_source_is_live AND s.invoice_header_id = v_source_invoice_id)
OR (NOT v_source_is_live AND s.draft_invoice_header_id = v_source_invoice_id)
)
ORDER BY s.starts_from DESC
LIMIT 1;
-- ✅ 4. Fetch source header from appropriate table
IF v_source_is_live THEN
SELECT * INTO src_header
FROM public.invoice_headers
WHERE id = v_source_invoice_id
AND is_deleted = false;
ELSE
SELECT * INTO src_header
FROM public.draft_invoice_headers
WHERE id = v_source_invoice_id
AND is_deleted = false;
END IF;
IF NOT FOUND THEN
INSERT INTO public.schedule_execution_logs (
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
) VALUES (
v_execution_log_id, v_related_schedule_id, (p_schedule_date::date), NOW(),
'Failure', 'Source invoice header not found in live or draft tables.', v_system_user_id, false
);
RAISE NOTICE 'Source invoice header % not found; aborting for date %.', v_source_invoice_id, (p_schedule_date::date);
RETURN;
END IF;
-- ✅ 5. Generate new draft invoice header
v_new_draft_invoice_id := gen_random_uuid();
CALL public.create_draft_invoice_header(
v_new_draft_invoice_id,
src_header.company_id,
src_header.customer_id,
src_header.debit_account_id,
src_header.credit_account_id,
(p_schedule_date::date),
((p_schedule_date::date) + COALESCE(src_header.payment_term, 10)),
src_header.payment_term,
src_header.taxable_amount,
src_header.cgst_amount,
src_header.sgst_amount,
src_header.igst_amount,
src_header.total_amount,
src_header.discount,
src_header.fees,
src_header.round_off,
src_header.currency_id,
src_header.note,
1,
v_system_user_id,
src_header.invoice_voucher,
src_header.source_warehouse_id,
src_header.destination_warehouse_id,
src_header.so_no,
(p_schedule_date::date),
src_header.type
);
-- ✅ 6. Clone all details
IF v_source_is_live THEN
FOR src_detail IN
SELECT * FROM public.invoice_details
WHERE invoice_header_id = v_source_invoice_id
LOOP
CALL public.create_draft_invoice_detail(
v_new_draft_invoice_id,
src_detail.price, src_detail.quantity, src_detail.discount, src_detail.fees,
src_detail.cgst_amount, src_detail.igst_amount, src_detail.sgst_amount,
src_detail.taxable_amount, src_detail.total_amount,
src_detail.serial_number, src_detail.product_id
);
END LOOP;
ELSE
FOR src_detail IN
SELECT * FROM public.draft_invoice_details
WHERE invoice_header_id = v_source_invoice_id
LOOP
CALL public.create_draft_invoice_detail(
v_new_draft_invoice_id,
src_detail.price, src_detail.quantity, src_detail.discount, src_detail.fees,
src_detail.cgst_amount, src_detail.igst_amount, src_detail.sgst_amount,
src_detail.taxable_amount, src_detail.total_amount,
src_detail.serial_number, src_detail.product_id
);
END LOOP;
END IF;
-- ✅ 7. Log success
INSERT INTO public.schedule_execution_logs (
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
) VALUES (
v_execution_log_id, v_related_schedule_id, (p_schedule_date::date), NOW(),
'Success', '', v_system_user_id, false
);
RAISE NOTICE 'Draft invoice % created from % (live=%), schedule_id %, on %.',
v_new_draft_invoice_id, v_source_invoice_id, v_source_is_live, v_related_schedule_id, (p_schedule_date::date);
EXCEPTION WHEN OTHERS THEN
INSERT INTO public.schedule_execution_logs (
id, schedule_id, execution_date, execution_time, status, error_message, triggered_by, is_deleted
) VALUES (
COALESCE(v_execution_log_id, gen_random_uuid()), v_related_schedule_id, (p_schedule_date::date), NOW(),
'Failure', SQLERRM, v_system_user_id, false
);
RAISE NOTICE 'Error while cloning invoice % on %: %',
v_source_invoice_id, (p_schedule_date::date), SQLERRM;
END;
$procedure$
-- Procedure: insert_draft_invoice
CREATE OR REPLACE PROCEDURE public.insert_draft_invoice(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_discount numeric, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_payment_term integer, IN p_invoice_status_id integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_note text, IN p_round_off numeric, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_so_no text, IN p_so_date timestamp without time zone, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_invoice_voucher text, IN p_created_by uuid, IN p_invoice_details jsonb, IN p_type integer, IN p_settled_amount numeric DEFAULT 0)
LANGUAGE plpgsql
AS $procedure$
DECLARE
detail RECORD;
v_source_warehouse_id uuid;
v_destination_warehouse_id uuid;
BEGIN
-- Set default values for warehouses
v_source_warehouse_id := COALESCE(p_source_warehouse_id, NULL);
v_destination_warehouse_id := COALESCE(p_destination_warehouse_id, NULL);
-- Insert into the invoice header
CALL public.create_draft_invoice_header (
p_id,
p_company_id,
p_customer_id,
p_credit_account_id,
p_debit_account_id,
p_invoice_date::date,
p_due_date::date,
p_payment_term,
p_taxable_amount,
p_cgst_amount,
p_sgst_amount,
p_igst_amount,
p_total_amount,
p_discount,
p_fees,
p_round_off,
p_currency_id,
p_note,
p_invoice_status_id,
p_created_by,
p_invoice_voucher,
v_source_warehouse_id,
v_destination_warehouse_id,
p_so_no,
p_so_date::date,
p_type
);
RAISE NOTICE 'Invoice header inserted with ID: %', p_id;
-- Loop through the JSONB array of invoice details
FOR detail IN
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
id uuid, product_id uuid, price numeric, quantity numeric,
fees numeric, discount numeric, taxable_amount numeric,
sgst_amount numeric, cgst_amount numeric, igst_amount numeric,
total_amount numeric, serial_number integer
)
LOOP
-- Insert into invoice details
INSERT INTO public.draft_invoice_details (
id, invoice_header_id, product_id, price, quantity, fees, discount,
taxable_amount, sgst_amount, cgst_amount, igst_amount, total_amount,
serial_number, is_deleted
) VALUES (
detail.id, p_id, detail.product_id, detail.price, detail.quantity, detail.fees,
detail.discount, detail.taxable_amount, detail.sgst_amount, detail.cgst_amount,
detail.igst_amount, detail.total_amount, detail.serial_number, false
);
RAISE NOTICE 'Invoice detail inserted with ID: %', detail.id;
END LOOP;
-- No explicit COMMIT here
RAISE NOTICE 'Transaction completed successfully';
EXCEPTION
-- Handle unique violation (duplicate key)
WHEN unique_violation THEN
RAISE NOTICE 'Duplicate entry: %, rolling back', SQLERRM;
ROLLBACK;
-- Handle foreign key violation
WHEN foreign_key_violation THEN
RAISE NOTICE 'Foreign key violation: %, rolling back', SQLERRM;
ROLLBACK;
-- Catch all other exceptions
WHEN OTHERS THEN
RAISE NOTICE 'An error occurred: %, transaction rolled back', SQLERRM;
ROLLBACK;
END;
$procedure$
-- Procedure: create_schedule_invoices
CREATE OR REPLACE PROCEDURE public.create_schedule_invoices(IN p_company_id uuid, IN p_frequency_cycle text, IN p_starts_from date, IN p_end_date date, IN p_created_by uuid, IN p_invoice_header_id uuid DEFAULT NULL::uuid, IN p_draft_invoice_header_id uuid DEFAULT NULL::uuid, IN p_month_of_year integer DEFAULT NULL::integer, IN p_day_of_month integer DEFAULT NULL::integer, IN p_day_of_week integer DEFAULT NULL::integer, IN p_time_of_day interval DEFAULT '00:00:00'::interval)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_recurring_schedule_id uuid := gen_random_uuid(); -- New ID for recurring_sales_schedules
v_schedule_status text;
-- normalized copies (treat all-zero GUID as NULL)
v_invoice_header_id uuid := NULLIF(p_invoice_header_id, '00000000-0000-0000-0000-000000000000'::uuid);
v_draft_invoice_header_id uuid := NULLIF(p_draft_invoice_header_id, '00000000-0000-0000-0000-000000000000'::uuid);
v_day_of_week integer := COALESCE(p_day_of_week, 0);
v_day_of_month integer := COALESCE(p_day_of_month, 0);
v_month_of_year integer := COALESCE(p_month_of_year, 0);
v_time_of_day interval := COALESCE(p_time_of_day, '00:00:00'::interval);
BEGIN
----------------------------------------------------------------
-- Validate: exactly ONE of invoice_header_id / draft_invoice_header_id
----------------------------------------------------------------
IF (v_invoice_header_id IS NULL AND v_draft_invoice_header_id IS NULL) THEN
RAISE EXCEPTION 'Either invoice_header_id or draft_invoice_header_id must be provided.';
ELSIF (v_invoice_header_id IS NOT NULL AND v_draft_invoice_header_id IS NOT NULL) THEN
RAISE EXCEPTION 'Provide only one of invoice_header_id or draft_invoice_header_id, not both.';
END IF;
-- Determine the initial schedule status
IF p_starts_from > CURRENT_DATE THEN
v_schedule_status := 'inactive';
ELSE
v_schedule_status := 'active';
END IF;
-- Insert into the recurring_sales_schedules table
INSERT INTO public.recurring_sales_schedules (
id, invoice_header_id, draft_invoice_header_id, company_id, frequency_cycle, schedule_status, starts_from, end_date,
created_on_utc, created_by, is_deleted
)
VALUES (
v_recurring_schedule_id, v_invoice_header_id, v_draft_invoice_header_id, p_company_id, p_frequency_cycle, v_schedule_status,
p_starts_from, p_end_date, NOW(), p_created_by, false
);
-- Insert into the public.recurrence_schedule_details table based on p_frequency_cycle
IF p_frequency_cycle = 'Daily' THEN
INSERT INTO public.recurrence_schedule_details (
schedule_id , frequency_cycle, time_of_day, is_deleted
)
VALUES (
v_recurring_schedule_id, p_frequency_cycle, v_time_of_day, false
);
ELSIF p_frequency_cycle = 'Weekly' THEN
INSERT INTO public.recurrence_schedule_details (
schedule_id, frequency_cycle, day_of_week, time_of_day, is_deleted
)
VALUES (
v_recurring_schedule_id, p_frequency_cycle, v_day_of_week, v_time_of_day, false
);
ELSIF p_frequency_cycle = 'Monthly' THEN
INSERT INTO public.recurrence_schedule_details (
schedule_id, frequency_cycle, day_of_month, time_of_day, is_deleted
)
VALUES (
v_recurring_schedule_id, p_frequency_cycle, v_day_of_month, v_time_of_day, false
);
ELSIF p_frequency_cycle = 'Yearly' THEN
INSERT INTO public.recurrence_schedule_details (
schedule_id, frequency_cycle, month_of_year, day_of_month, time_of_day, is_deleted
)
VALUES (
v_recurring_schedule_id, p_frequency_cycle, v_month_of_year, v_day_of_month, v_time_of_day, false
);
ELSE
RAISE NOTICE 'Invalid frequency cycle: %', p_frequency_cycle;
END IF;
RAISE NOTICE 'Recurring schedule created successfully with ID: %, Status: %', v_recurring_schedule_id, v_schedule_status;
END;
$procedure$
-- Procedure: insert_invoice
CREATE OR REPLACE PROCEDURE public.insert_invoice(IN p_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_discount numeric, IN p_currency_id integer, IN p_invoice_date timestamp without time zone, IN p_payment_term integer, IN p_invoice_status_id integer, IN p_due_date timestamp without time zone, IN p_total_amount numeric, IN p_taxable_amount numeric, IN p_fees numeric, IN p_sgst_amount numeric, IN p_cgst_amount numeric, IN p_igst_amount numeric, IN p_note text, IN p_round_off numeric, IN p_debit_account_id uuid, IN p_credit_account_id uuid, IN p_so_no text, IN p_so_date timestamp without time zone, IN p_source_warehouse_id uuid, IN p_destination_warehouse_id uuid, IN p_invoice_voucher text, IN p_created_by uuid, IN p_invoice_details jsonb, IN p_type integer, IN p_settled_amount numeric DEFAULT 0)
LANGUAGE plpgsql
AS $procedure$
DECLARE
detail RECORD;
v_source_warehouse_id uuid;
v_destination_warehouse_id uuid;
BEGIN
-- Set default values for warehouses
v_source_warehouse_id := COALESCE(p_source_warehouse_id, NULL);
v_destination_warehouse_id := COALESCE(p_destination_warehouse_id, NULL);
-- Insert into the invoice header
CALL public.create_invoice_header (
p_id,
p_company_id,
p_customer_id,
p_credit_account_id,
p_debit_account_id,
p_invoice_date::date,
p_due_date::date,
p_payment_term,
p_taxable_amount,
p_cgst_amount,
p_sgst_amount,
p_igst_amount,
p_total_amount,
p_discount,
p_fees,
p_round_off,
p_currency_id,
p_note,
p_invoice_status_id,
p_created_by,
p_invoice_voucher,
v_source_warehouse_id,
v_destination_warehouse_id,
p_so_no,
p_so_date::date,
p_type
);
RAISE NOTICE 'Invoice header inserted with ID: %', p_id;
-- Loop through the JSONB array of invoice details
FOR detail IN
SELECT * FROM jsonb_to_recordset(p_invoice_details) AS (
id uuid, product_id uuid, price numeric, quantity numeric,
fees numeric, discount numeric, taxable_amount numeric,
sgst_amount numeric, cgst_amount numeric, igst_amount numeric,
total_amount numeric, serial_number integer
)
LOOP
-- Insert into invoice details
INSERT INTO public.invoice_details (
id, invoice_header_id, product_id, price, quantity, fees, discount,
taxable_amount, sgst_amount, cgst_amount, igst_amount, total_amount,
serial_number, is_deleted
) VALUES (
detail.id, p_id, detail.product_id, detail.price, detail.quantity, detail.fees,
detail.discount, detail.taxable_amount, detail.sgst_amount, detail.cgst_amount,
detail.igst_amount, detail.total_amount, detail.serial_number, false
);
RAISE NOTICE 'Invoice detail inserted with ID: %', detail.id;
END LOOP;
-- No explicit COMMIT here
RAISE NOTICE 'Transaction completed successfully';
EXCEPTION
-- Handle unique violation (duplicate key)
WHEN unique_violation THEN
RAISE NOTICE 'Duplicate entry: %, rolling back', SQLERRM;
ROLLBACK;
-- Handle foreign key violation
WHEN foreign_key_violation THEN
RAISE NOTICE 'Foreign key violation: %, rolling back', SQLERRM;
ROLLBACK;
-- Catch all other exceptions
WHEN OTHERS THEN
RAISE NOTICE 'An error occurred: %, transaction rolled back', SQLERRM;
ROLLBACK;
END;
$procedure$
-- Procedure: insert_multiple_invoices_by_excel
CREATE OR REPLACE PROCEDURE public.insert_multiple_invoices_by_excel(IN p_invoices jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
invoice RECORD;
v_created_by uuid;
results TEXT[] := ARRAY[]::TEXT[];
record_status TEXT;
BEGIN
-- Optional: default created_by fallback
SELECT id INTO v_created_by
FROM users
WHERE first_name = 'System'
LIMIT 1;
FOR invoice IN
SELECT * FROM jsonb_to_recordset(p_invoices) AS (
invoice_id uuid,
company_id uuid,
customer_id uuid,
discount numeric,
currency_id integer,
invoice_date timestamp without time zone,
invoice_number text,
payment_term integer,
invoice_status_id integer,
due_date timestamp without time zone,
total_amount numeric,
taxable_amount numeric,
fees numeric,
sgst_amount numeric,
cgst_amount numeric,
igst_amount numeric,
note text,
round_off numeric,
debit_account_id uuid,
credit_account_id uuid,
so_no text,
so_date timestamp without time zone,
source_warehouse_id uuid,
destination_warehouse_id uuid,
invoice_voucher text,
created_by uuid,
lines jsonb,
type integer,
settled_amount numeric
)
LOOP
BEGIN
-- Duplicate? mark & skip
IF EXISTS (
SELECT 1
FROM public.invoice_headers
WHERE invoice_number = invoice.invoice_number
AND company_id = invoice.company_id
) THEN
record_status := 'duplicate';
results := array_append(results, invoice.invoice_number || ':' || record_status);
CONTINUE;
END IF;
-- Insert via your existing routine
CALL public.insert_invoice_by_excel(
invoice.invoice_id,
invoice.company_id,
invoice.customer_id,
invoice.discount,
invoice.currency_id,
invoice.invoice_date,
invoice.invoice_number,
invoice.payment_term,
invoice.invoice_status_id,
invoice.due_date,
invoice.total_amount,
invoice.taxable_amount,
invoice.fees,
invoice.sgst_amount,
invoice.cgst_amount,
invoice.igst_amount,
invoice.note,
invoice.round_off,
invoice.debit_account_id,
invoice.credit_account_id,
invoice.so_no,
invoice.so_date,
invoice.source_warehouse_id,
invoice.destination_warehouse_id,
invoice.invoice_voucher,
COALESCE(invoice.created_by, v_created_by),
invoice.lines::jsonb,
invoice.type,
invoice.settled_amount
);
record_status := 'success';
results := array_append(results, invoice.invoice_number || ':' || record_status);
EXCEPTION WHEN OTHERS THEN
record_status := 'failed';
results := array_append(results, invoice.invoice_number || ':' || record_status);
CONTINUE;
END;
END LOOP;
-- Emit overall status as before
IF array_position(results, '%:failed') IS NOT NULL THEN
RAISE NOTICE 'STATUS:error';
ELSIF array_position(results, '%:duplicate') IS NOT NULL THEN
RAISE NOTICE 'STATUS:duplicate';
ELSE
RAISE NOTICE 'STATUS:success';
END IF;
-- Emit per-record status (as a JSONB array for convenience)
RAISE NOTICE 'DETAILS:%', to_jsonb(results);
END;
$procedure$
-- Procedure: create_invoice_payment
CREATE OR REPLACE PROCEDURE public.create_invoice_payment(IN p_invoice_payment_header_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_received_date date, IN p_mode_of_payment text, IN p_credit_account_id uuid, IN p_debit_account_id uuid, IN p_reference text, IN p_received_amount numeric, IN p_grand_total_amount numeric, IN p_advance_amount numeric, IN p_description text, IN p_tds_amount numeric, IN p_created_by uuid, IN p_invoice_payment_details jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_total_paid_amount NUMERIC;
v_settled_amount NUMERIC;
v_invoice_status_id INT;
v_payment_status_id INT;
v_invoice_header_id UUID;
v_payment_amount NUMERIC;
v_tds_amount NUMERIC;
v_serial_number INT;
v_row JSONB;
v_payment_number VARCHAR;
v_invoice_ids UUID[] := '{}';
BEGIN
v_payment_number := get_new_payment_number(p_company_id, p_received_date);
INSERT INTO public.invoice_payment_headers (
id,
company_id,
customer_id,
received_date,
mode_of_payment,
credit_account_id,
debit_account_id,
reference,
received_amount,
description,
tds_amount,
advance_amount,
grand_total_amount,
payment_number,
created_by,
created_on_utc,
is_deleted
)
VALUES (
p_invoice_payment_header_id,
p_company_id,
p_customer_id,
p_received_date,
p_mode_of_payment,
p_credit_account_id,
p_debit_account_id,
p_reference,
p_received_amount,
p_description,
p_tds_amount,
p_advance_amount,
p_grand_total_amount,
v_payment_number,
p_created_by,
now(),
false
);
FOR v_row IN SELECT * FROM jsonb_array_elements(p_invoice_payment_details)
LOOP
v_invoice_header_id := (v_row->>'header_id')::UUID;
v_payment_amount := COALESCE((v_row->>'payment_amount')::NUMERIC, 0);
v_tds_amount := COALESCE((v_row->>'tds_amount')::NUMERIC, 0);
v_serial_number := (v_row->>'serial_number')::INT;
v_total_paid_amount := 0;
v_payment_status_id := 1;
v_invoice_status_id := 1;
INSERT INTO public.invoice_payment_details (
id,
invoice_payment_header_id,
invoice_header_id,
received_amount,
tds_amount,
serial_number,
created_by,
created_on_utc
)
VALUES (
(v_row->>'id')::UUID,
p_invoice_payment_header_id,
v_invoice_header_id,
v_payment_amount,
v_tds_amount,
v_serial_number,
p_created_by,
now()
);
SELECT settled_amount INTO v_settled_amount
FROM public.invoice_headers
WHERE id = v_invoice_header_id;
v_total_paid_amount := COALESCE(v_settled_amount, 0) + v_payment_amount + v_tds_amount;
IF (SELECT total_amount FROM public.invoice_headers WHERE id = v_invoice_header_id) > v_total_paid_amount THEN
v_invoice_status_id := 4; -- PARTIALLY_PAID
v_payment_status_id := 2; -- PartiallyPaid
ELSE
v_invoice_status_id := 5; -- PAID
v_payment_status_id := 3; -- FullyPaid
END IF;
UPDATE public.invoice_headers
SET settled_amount = v_total_paid_amount,
invoice_status_id = v_invoice_status_id,
payment_status_id = v_payment_status_id
WHERE id = v_invoice_header_id;
v_invoice_ids := array_append(v_invoice_ids, v_invoice_header_id);
END LOOP;
IF array_length(v_invoice_ids, 1) > 0 THEN
CALL public.update_invoice_next_status_for_invoice_header(
p_company_id,
v_invoice_ids,
p_created_by
);
END IF;
RAISE NOTICE 'Invoice payment inserted successfully';
END;
$procedure$
-- Procedure: create_invoice_payment
CREATE OR REPLACE PROCEDURE public.create_invoice_payment(IN p_invoice_payment_header_id uuid, IN p_company_id uuid, IN p_customer_id uuid, IN p_received_date date, IN p_mode_of_payment text, IN p_credit_account_id uuid, IN p_debit_account_id uuid, IN p_reference text, IN p_received_amount numeric, IN p_grand_total_amount numeric, IN p_advance_amount numeric, IN p_description text, IN p_tds_amount numeric, IN p_created_by uuid, IN p_payment_status_id integer, IN p_invoice_payment_details jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_total_paid_amount NUMERIC;
v_settled_amount NUMERIC;
v_invoice_status_id INT;
v_payment_status_id INT;
v_invoice_header_id UUID;
v_payment_amount NUMERIC;
v_tds_amount NUMERIC;
v_serial_number INT;
v_row JSONB;
v_payment_number VARCHAR;
v_invoice_ids UUID[] := '{}';
BEGIN
-- Generate payment number
v_payment_number := get_new_payment_number(p_company_id, p_received_date);
-- Insert payment header
INSERT INTO public.invoice_payment_headers (
id,
company_id,
customer_id,
received_date,
mode_of_payment,
credit_account_id,
debit_account_id,
reference,
received_amount,
description,
tds_amount,
advance_amount,
grand_total_amount,
payment_number,
created_by,
created_on_utc,
is_deleted,
payment_status_id
)
VALUES (
p_invoice_payment_header_id,
p_company_id,
p_customer_id,
p_received_date,
p_mode_of_payment,
p_credit_account_id,
p_debit_account_id,
p_reference,
p_received_amount,
p_description,
p_tds_amount,
p_advance_amount,
p_grand_total_amount,
v_payment_number,
p_created_by,
now(),
false,
p_payment_status_id
);
-- Process invoice-wise payments
FOR v_row IN SELECT * FROM jsonb_array_elements(p_invoice_payment_details)
LOOP
v_invoice_header_id := (v_row->>'header_id')::UUID;
v_payment_amount := COALESCE((v_row->>'payment_amount')::NUMERIC, 0);
v_tds_amount := COALESCE((v_row->>'tds_amount')::NUMERIC, 0);
v_serial_number := (v_row->>'serial_number')::INT;
INSERT INTO public.invoice_payment_details (
id,
invoice_payment_header_id,
invoice_header_id,
received_amount,
tds_amount,
serial_number,
created_by,
created_on_utc
)
VALUES (
(v_row->>'id')::UUID,
p_invoice_payment_header_id,
v_invoice_header_id,
v_payment_amount,
v_tds_amount,
v_serial_number,
p_created_by,
now()
);
SELECT settled_amount
INTO v_settled_amount
FROM public.invoice_headers
WHERE id = v_invoice_header_id;
v_total_paid_amount :=
COALESCE(v_settled_amount, 0)
+ v_payment_amount
+ v_tds_amount;
IF (SELECT total_amount FROM public.invoice_headers WHERE id = v_invoice_header_id)
> v_total_paid_amount THEN
v_invoice_status_id := 4; -- Partially Paid
v_payment_status_id := 2; -- PartiallyPaid
ELSE
v_invoice_status_id := 5; -- Paid
v_payment_status_id := 3; -- FullyPaid
END IF;
UPDATE public.invoice_headers
SET
settled_amount = v_total_paid_amount,
invoice_status_id = v_invoice_status_id,
payment_status_id = v_payment_status_id
WHERE id = v_invoice_header_id;
v_invoice_ids := array_append(v_invoice_ids, v_invoice_header_id);
END LOOP;
-- Move invoice to next workflow status
IF array_length(v_invoice_ids, 1) > 0 THEN
CALL public.update_invoice_next_status_for_invoice_header(
p_company_id,
v_invoice_ids,
p_created_by
);
END IF;
RAISE NOTICE 'Invoice payment inserted successfully';
END;
$procedure$
-- Procedure: insert_delinquency_snapshot
CREATE OR REPLACE PROCEDURE public.insert_delinquency_snapshot(IN p_organization_id uuid, IN p_customer_id uuid, IN p_cycle_id uuid, IN p_overdue_amount numeric, IN p_oldest_due_date date, IN p_days_past_due integer, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- 🛑 Guard: one snapshot per cycle per day
IF EXISTS (
SELECT 1
FROM delinquency_snapshots ds
WHERE ds.cycle_id = p_cycle_id
AND ds.created_on_utc::date = CURRENT_DATE
AND ds.is_deleted = false
) THEN
RETURN;
END IF;
INSERT INTO delinquency_snapshots (
id,
organization_id,
customer_id,
cycle_id,
overdue_amount,
oldest_due_date,
days_past_due,
aging_bucket,
last_evaluated_on,
created_on_utc,
created_by,
is_deleted
)
VALUES (
gen_random_uuid(),
p_organization_id,
p_customer_id,
p_cycle_id,
p_overdue_amount,
p_oldest_due_date,
p_days_past_due,
get_aging_bucket(p_days_past_due),
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
p_created_by,
false
);
END;
$procedure$
-- View: invoice_approval_level_view
SELECT company_id,
account_id,
status_id,
COALESCE(( SELECT invoice_account_approval_levels.approval_level_required
FROM invoice_account_approval_levels
WHERE ((invoice_account_approval_levels.company_id = i.company_id) AND (invoice_account_approval_levels.account_id = i.account_id) AND (invoice_account_approval_levels.status_id = i.status_id) AND (invoice_account_approval_levels.is_deleted = false))
LIMIT 1), ( SELECT invoice_workflow.approval_level
FROM invoice_workflow
WHERE ((invoice_workflow.company_id = i.company_id) AND (invoice_workflow.status = i.status_id) AND (invoice_workflow.is_deleted = false) AND (invoice_workflow.is_enabled = true))
LIMIT 1)) AS approval_level_required
FROM ( SELECT invoice_account_approval_levels.company_id,
invoice_account_approval_levels.account_id,
invoice_account_approval_levels.status_id
FROM invoice_account_approval_levels
UNION
SELECT invoice_workflow.company_id,
NULL::uuid AS account_id,
invoice_workflow.status AS status_id
FROM invoice_workflow) i;
-- View: vw_invoice_approval_permissions
SELECT COALESCE(iaua.company_id, iauc.company_id) AS company_id,
COALESCE(iaua.status_id, iauc.status_id) AS status_id,
COALESCE(iaua.user_id, iauc.user_id) AS user_id,
COALESCE(iaua.approval_level, iauc.approval_level) AS approval_level,
iaua.account_id,
iaua.approval_level AS account_approval_level,
iaua.status_id AS account_status_id
FROM (invoice_approval_user_company iauc
LEFT JOIN invoice_approval_users_account iaua ON (((iauc.company_id = iaua.company_id) AND (iauc.status_id = iaua.status_id))))
WHERE (((iaua.company_id = iauc.company_id) AND (iaua.status_id = iauc.status_id)) OR ((iaua.company_id IS NULL) AND (iauc.is_deleted = false) AND (iaua.is_deleted = false)));
-- View: v_run_defaulter_fdw
SELECT result
FROM run_defaulter_fdw() run_defaulter_fdw(result);