feat: Enhance time tracking by excluding billed entries from views and approval processes
This commit is contained in:
parent
ccb7714779
commit
19827d03a8
@ -1092,6 +1092,16 @@ async def get_customer_time_entries(customer_id: int, status: Optional[str] = No
|
|||||||
LEFT JOIN tmodule_cases c ON t.case_id = c.id
|
LEFT JOIN tmodule_cases c ON t.case_id = c.id
|
||||||
LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
|
LEFT JOIN tmodule_customers cust ON t.customer_id = cust.id
|
||||||
WHERE t.customer_id = %s
|
WHERE t.customer_id = %s
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
params = [customer_id]
|
params = [customer_id]
|
||||||
|
|||||||
@ -670,17 +670,18 @@ class TimeTrackingVTigerService:
|
|||||||
- timelognumber: Unique ID (TL1234)
|
- timelognumber: Unique ID (TL1234)
|
||||||
- duration: Time in seconds
|
- duration: Time in seconds
|
||||||
- relatedto: Reference to Case/Account
|
- relatedto: Reference to Case/Account
|
||||||
- is_billable: '1' = yes, '0' = no
|
- isbillable: '1' = yes, '0' = no
|
||||||
- cf_timelog_invoiced: '1' = has been invoiced
|
- cf_timelog_invoiced: '1' = has been invoiced
|
||||||
|
- billed_via_thehub_id: Hub order ID (in vTiger custom field)
|
||||||
|
|
||||||
We only sync entries where:
|
We only sync entries where:
|
||||||
- relatedto is not empty (linked to a Case or Account)
|
- relatedto is not empty (linked to a Case or Account)
|
||||||
- Has valid duration > 0
|
- Has valid duration > 0
|
||||||
|
- isbillable = '1' (only billable entries)
|
||||||
NOTE: is_billable and cf_timelog_invoiced fields are not reliably populated in vTiger,
|
- cf_timelog_invoiced = '0' or NULL (not yet invoiced in vTiger)
|
||||||
so we sync all timelogs and let the approval workflow decide what to bill.
|
- billed_via_thehub_id = '0' or NULL (not yet billed via Hub)
|
||||||
"""
|
"""
|
||||||
logger.info(f"🔍 Syncing all timelogs from vTiger with valid relatedto...")
|
logger.info(f"🔍 Syncing billable, uninvoiced timelogs from vTiger...")
|
||||||
|
|
||||||
stats = {"imported": 0, "updated": 0, "skipped": 0, "errors": 0}
|
stats = {"imported": 0, "updated": 0, "skipped": 0, "errors": 0}
|
||||||
|
|
||||||
@ -716,12 +717,49 @@ class TimeTrackingVTigerService:
|
|||||||
|
|
||||||
logger.info(f"✅ Total fetched: {len(all_timelogs)} Timelog entries from vTiger")
|
logger.info(f"✅ Total fetched: {len(all_timelogs)} Timelog entries from vTiger")
|
||||||
|
|
||||||
# We don't filter here - the existing code already filters by:
|
# Filter timelogs based on requirements:
|
||||||
# 1. duration > 0
|
# 1. isbillable = '1' (only billable)
|
||||||
# 2. relatedto not empty
|
# 2. cf_timelog_invoiced = '0' or NULL (not invoiced in vTiger)
|
||||||
# These filters happen in the processing loop below
|
# 3. billed_via_thehub_id = '0' or NULL (not billed via Hub)
|
||||||
|
# 4. cf_timelog_rounduptimespent = '0' or NULL (not manually invoiced in vTiger)
|
||||||
|
filtered_timelogs = []
|
||||||
|
filter_stats = {"not_billable": 0, "already_invoiced": 0, "billed_via_hub": 0, "manually_invoiced": 0}
|
||||||
|
|
||||||
timelogs = all_timelogs[:limit] # Trim to requested limit
|
for tl in all_timelogs:
|
||||||
|
isbillable = tl.get('isbillable', '0')
|
||||||
|
invoiced = tl.get('cf_timelog_invoiced', '0') or '0'
|
||||||
|
billed_via_hub = tl.get('billed_via_thehub_id', '0') or '0'
|
||||||
|
billed_via_hub_vtiger = tl.get('cf_timelog_billedviathehubid', '0') or '0'
|
||||||
|
roundup_spent = tl.get('cf_timelog_rounduptimespent', '0') or '0'
|
||||||
|
|
||||||
|
# Debug log first 3 entries to see actual values
|
||||||
|
if len(all_timelogs) <= 3 or len(filtered_timelogs) == 0:
|
||||||
|
logger.info(f"🔍 DEBUG Timelog {tl.get('id')}: isbillable={isbillable}, cf_timelog_invoiced={invoiced}, billed_via_thehub_id={billed_via_hub}, billed_via_hub_vtiger={billed_via_hub_vtiger}, roundup_spent={roundup_spent}")
|
||||||
|
|
||||||
|
# Track why we skip
|
||||||
|
if isbillable != '1':
|
||||||
|
filter_stats["not_billable"] += 1
|
||||||
|
continue
|
||||||
|
if invoiced not in ('0', '', None):
|
||||||
|
filter_stats["already_invoiced"] += 1
|
||||||
|
continue
|
||||||
|
if billed_via_hub not in ('0', '', None):
|
||||||
|
filter_stats["billed_via_hub"] += 1
|
||||||
|
continue
|
||||||
|
if billed_via_hub_vtiger not in ('0', '', None):
|
||||||
|
filter_stats["billed_via_hub"] += 1
|
||||||
|
continue
|
||||||
|
if roundup_spent not in ('0', '', None):
|
||||||
|
filter_stats["manually_invoiced"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Only include if billable AND not invoiced AND not billed via Hub AND not manually invoiced
|
||||||
|
filtered_timelogs.append(tl)
|
||||||
|
|
||||||
|
logger.info(f"🔍 Filtered to {len(filtered_timelogs)} billable, uninvoiced timelogs")
|
||||||
|
logger.info(f" Skipped: {filter_stats['not_billable']} not billable, {filter_stats['already_invoiced']} already invoiced, {filter_stats['billed_via_hub']} billed via Hub, {filter_stats['manually_invoiced']} manually invoiced")
|
||||||
|
|
||||||
|
timelogs = filtered_timelogs[:limit] # Trim to requested limit
|
||||||
logger.info(f"📊 Processing {len(timelogs)} timelogs...")
|
logger.info(f"📊 Processing {len(timelogs)} timelogs...")
|
||||||
|
|
||||||
# NOTE: retrieve API is too slow for batch operations (1500+ individual calls)
|
# NOTE: retrieve API is too slow for batch operations (1500+ individual calls)
|
||||||
|
|||||||
@ -172,6 +172,13 @@ class WizardService:
|
|||||||
detail=f"Time entry already {entry['status']}"
|
detail=f"Time entry already {entry['status']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if already billed
|
||||||
|
if entry.get('billed_via_thehub_id') is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Cannot approve time entry that has already been billed"
|
||||||
|
)
|
||||||
|
|
||||||
# Update entry
|
# Update entry
|
||||||
logger.info(f"🔄 Updating time entry {approval.time_id} in database")
|
logger.info(f"🔄 Updating time entry {approval.time_id} in database")
|
||||||
update_query = """
|
update_query = """
|
||||||
@ -185,6 +192,7 @@ class WizardService:
|
|||||||
approved_at = CURRENT_TIMESTAMP,
|
approved_at = CURRENT_TIMESTAMP,
|
||||||
approved_by = %s
|
approved_by = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
|
AND billed_via_thehub_id IS NULL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
execute_update(
|
execute_update(
|
||||||
@ -265,6 +273,13 @@ class WizardService:
|
|||||||
detail=f"Time entry already {entry['status']}"
|
detail=f"Time entry already {entry['status']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if already billed
|
||||||
|
if entry.get('billed_via_thehub_id') is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Cannot reject time entry that has already been billed"
|
||||||
|
)
|
||||||
|
|
||||||
# Update to rejected
|
# Update to rejected
|
||||||
update_query = """
|
update_query = """
|
||||||
UPDATE tmodule_times
|
UPDATE tmodule_times
|
||||||
@ -274,6 +289,7 @@ class WizardService:
|
|||||||
approved_at = CURRENT_TIMESTAMP,
|
approved_at = CURRENT_TIMESTAMP,
|
||||||
approved_by = %s
|
approved_by = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
|
AND billed_via_thehub_id IS NULL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
execute_update(update_query, (reason, user_id, time_id))
|
execute_update(update_query, (reason, user_id, time_id))
|
||||||
@ -341,6 +357,13 @@ class WizardService:
|
|||||||
detail="Cannot reset billed entries"
|
detail="Cannot reset billed entries"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if already billed via Hub order
|
||||||
|
if entry.get('billed_via_thehub_id') is not None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Cannot reset time entry that has been billed through Hub order"
|
||||||
|
)
|
||||||
|
|
||||||
# Reset to pending - clear all approval data
|
# Reset to pending - clear all approval data
|
||||||
update_query = """
|
update_query = """
|
||||||
UPDATE tmodule_times
|
UPDATE tmodule_times
|
||||||
@ -352,6 +375,7 @@ class WizardService:
|
|||||||
approved_at = NULL,
|
approved_at = NULL,
|
||||||
approved_by = NULL
|
approved_by = NULL
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
|
AND billed_via_thehub_id IS NULL
|
||||||
"""
|
"""
|
||||||
|
|
||||||
execute_update(update_query, (reason, time_id))
|
execute_update(update_query, (reason, time_id))
|
||||||
@ -490,7 +514,9 @@ class WizardService:
|
|||||||
SELECT c.id, c.title
|
SELECT c.id, c.title
|
||||||
FROM tmodule_times t
|
FROM tmodule_times t
|
||||||
JOIN tmodule_cases c ON t.case_id = c.id
|
JOIN tmodule_cases c ON t.case_id = c.id
|
||||||
WHERE t.customer_id = %s AND t.status = 'pending'
|
WHERE t.customer_id = %s
|
||||||
|
AND t.status = 'pending'
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
ORDER BY t.worked_date
|
ORDER BY t.worked_date
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"""
|
"""
|
||||||
@ -558,6 +584,13 @@ class WizardService:
|
|||||||
AND t.status = 'pending'
|
AND t.status = 'pending'
|
||||||
AND t.billable = true
|
AND t.billable = true
|
||||||
AND t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
AND t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
AND cust.uses_time_card = false
|
AND cust.uses_time_card = false
|
||||||
ORDER BY t.worked_date, t.id
|
ORDER BY t.worked_date, t.id
|
||||||
"""
|
"""
|
||||||
@ -585,6 +618,13 @@ class WizardService:
|
|||||||
AND t.status = 'pending'
|
AND t.status = 'pending'
|
||||||
AND t.billable = true
|
AND t.billable = true
|
||||||
AND t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
AND t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
ORDER BY t.worked_date, t.id
|
ORDER BY t.worked_date, t.id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
41
migrations/061_update_timetracking_views_billed_filter.sql
Normal file
41
migrations/061_update_timetracking_views_billed_filter.sql
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
-- Migration: Update timetracking views to exclude billed entries
|
||||||
|
-- Ensures that time entries with billed_via_thehub_id are not shown in pending counts
|
||||||
|
|
||||||
|
-- Drop and recreate approval stats view to exclude billed entries
|
||||||
|
DROP VIEW IF EXISTS tmodule_approval_stats CASCADE;
|
||||||
|
CREATE VIEW tmodule_approval_stats AS
|
||||||
|
SELECT
|
||||||
|
c.id AS customer_id,
|
||||||
|
c.name AS customer_name,
|
||||||
|
c.vtiger_id AS customer_vtiger_id,
|
||||||
|
c.uses_time_card AS uses_time_card,
|
||||||
|
COUNT(t.id) FILTER (WHERE t.billable = true AND t.billed_via_thehub_id IS NULL) AS total_entries,
|
||||||
|
COUNT(t.id) FILTER (WHERE t.billable = true AND t.status = 'pending' AND t.billed_via_thehub_id IS NULL) AS pending_count,
|
||||||
|
COUNT(t.id) FILTER (WHERE t.billable = true AND t.status = 'approved' AND t.billed_via_thehub_id IS NULL) AS approved_count,
|
||||||
|
COUNT(t.id) FILTER (WHERE t.billable = true AND t.status = 'rejected') AS rejected_count,
|
||||||
|
COUNT(t.id) FILTER (WHERE t.billable = true AND t.status = 'billed') AS billed_count,
|
||||||
|
SUM(t.original_hours) FILTER (WHERE t.billable = true AND t.billed_via_thehub_id IS NULL) AS total_original_hours,
|
||||||
|
SUM(t.approved_hours) FILTER (WHERE t.billable = true AND t.status = 'approved' AND t.billed_via_thehub_id IS NULL) AS total_approved_hours,
|
||||||
|
MAX(t.worked_date) FILTER (WHERE t.billable = true AND t.billed_via_thehub_id IS NULL) AS latest_work_date,
|
||||||
|
MAX(t.last_synced_at) FILTER (WHERE t.billable = true AND t.billed_via_thehub_id IS NULL) AS last_sync
|
||||||
|
FROM tmodule_customers c
|
||||||
|
LEFT JOIN tmodule_times t ON c.id = t.customer_id
|
||||||
|
GROUP BY c.id, c.name, c.vtiger_id, c.uses_time_card;
|
||||||
|
|
||||||
|
-- Drop and recreate next pending view to exclude billed entries
|
||||||
|
DROP VIEW IF EXISTS tmodule_next_pending CASCADE;
|
||||||
|
CREATE VIEW tmodule_next_pending AS
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
|
||||||
|
c.status AS case_status,
|
||||||
|
c.vtiger_id AS case_vtiger_id,
|
||||||
|
cust.name AS customer_name,
|
||||||
|
cust.hourly_rate AS customer_rate
|
||||||
|
FROM tmodule_times t
|
||||||
|
JOIN tmodule_cases c ON t.case_id = c.id
|
||||||
|
JOIN tmodule_customers cust ON t.customer_id = cust.id
|
||||||
|
WHERE t.status = 'pending'
|
||||||
|
AND t.billable = true
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
ORDER BY cust.name, c.title, t.worked_date;
|
||||||
139
migrations/062_filter_invoiced_in_views.sql
Normal file
139
migrations/062_filter_invoiced_in_views.sql
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
-- Migration: Filter out vTiger-invoiced entries from approval views
|
||||||
|
-- Ensures that time entries already invoiced in vTiger are not shown in pending/approval flows
|
||||||
|
-- Also filters entries with cf_timelog_rounduptimespent set (manually invoiced in vTiger)
|
||||||
|
-- Also filters entries with cf_timelog_billedviathehubid set (billed via Hub in vTiger)
|
||||||
|
|
||||||
|
-- Drop and recreate approval stats view to exclude vTiger-invoiced entries
|
||||||
|
DROP VIEW IF EXISTS tmodule_approval_stats CASCADE;
|
||||||
|
CREATE VIEW tmodule_approval_stats AS
|
||||||
|
SELECT
|
||||||
|
c.id AS customer_id,
|
||||||
|
c.name AS customer_name,
|
||||||
|
c.vtiger_id AS customer_vtiger_id,
|
||||||
|
c.uses_time_card AS uses_time_card,
|
||||||
|
COUNT(t.id) FILTER (WHERE
|
||||||
|
t.billable = true
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
|
) AS total_entries,
|
||||||
|
COUNT(t.id) FILTER (WHERE
|
||||||
|
t.billable = true
|
||||||
|
AND t.status = 'pending'
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
|
) AS pending_count,
|
||||||
|
COUNT(t.id) FILTER (WHERE
|
||||||
|
t.billable = true
|
||||||
|
AND t.status = 'approved'
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
|
) AS approved_count,
|
||||||
|
COUNT(t.id) FILTER (WHERE t.billable = true AND t.status = 'rejected') AS rejected_count,
|
||||||
|
COUNT(t.id) FILTER (WHERE t.billable = true AND t.status = 'billed') AS billed_count,
|
||||||
|
SUM(t.original_hours) FILTER (WHERE
|
||||||
|
t.billable = true
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
|
) AS total_original_hours,
|
||||||
|
SUM(t.approved_hours) FILTER (WHERE
|
||||||
|
t.billable = true
|
||||||
|
AND t.status = 'approved'
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
|
) AS total_approved_hours,
|
||||||
|
MAX(t.worked_date) FILTER (WHERE
|
||||||
|
t.billable = true
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
|
) AS latest_work_date,
|
||||||
|
MAX(t.last_synced_at) FILTER (WHERE
|
||||||
|
t.billable = true
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
|
) AS last_sync
|
||||||
|
FROM tmodule_customers c
|
||||||
|
LEFT JOIN tmodule_times t ON c.id = t.customer_id
|
||||||
|
GROUP BY c.id, c.name, c.vtiger_id, c.uses_time_card;
|
||||||
|
|
||||||
|
-- Drop and recreate next pending view to exclude vTiger-invoiced entries
|
||||||
|
DROP VIEW IF EXISTS tmodule_next_pending CASCADE;
|
||||||
|
CREATE VIEW tmodule_next_pending AS
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
COALESCE(c.vtiger_data->>'case_no', c.title)::VARCHAR(500) AS case_title,
|
||||||
|
c.status AS case_status,
|
||||||
|
c.vtiger_id AS case_vtiger_id,
|
||||||
|
cust.name AS customer_name,
|
||||||
|
cust.hourly_rate AS customer_rate
|
||||||
|
FROM tmodule_times t
|
||||||
|
JOIN tmodule_cases c ON t.case_id = c.id
|
||||||
|
JOIN tmodule_customers cust ON t.customer_id = cust.id
|
||||||
|
WHERE t.status = 'pending'
|
||||||
|
AND t.billable = true
|
||||||
|
AND t.billed_via_thehub_id IS NULL
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_invoiced' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_invoiced' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_rounduptimespent' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_rounduptimespent' = '')
|
||||||
|
AND (t.vtiger_data->>'cf_timelog_billedviathehubid' IS NULL
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '0'
|
||||||
|
OR t.vtiger_data->>'cf_timelog_billedviathehubid' = '')
|
||||||
|
ORDER BY cust.name, c.title, t.worked_date;
|
||||||
Loading…
Reference in New Issue
Block a user