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_customers cust ON t.customer_id = cust.id
|
||||
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]
|
||||
|
||||
@ -670,17 +670,18 @@ class TimeTrackingVTigerService:
|
||||
- timelognumber: Unique ID (TL1234)
|
||||
- duration: Time in seconds
|
||||
- relatedto: Reference to Case/Account
|
||||
- is_billable: '1' = yes, '0' = no
|
||||
- isbillable: '1' = yes, '0' = no
|
||||
- cf_timelog_invoiced: '1' = has been invoiced
|
||||
- billed_via_thehub_id: Hub order ID (in vTiger custom field)
|
||||
|
||||
We only sync entries where:
|
||||
- relatedto is not empty (linked to a Case or Account)
|
||||
- Has valid duration > 0
|
||||
|
||||
NOTE: is_billable and cf_timelog_invoiced fields are not reliably populated in vTiger,
|
||||
so we sync all timelogs and let the approval workflow decide what to bill.
|
||||
- isbillable = '1' (only billable entries)
|
||||
- cf_timelog_invoiced = '0' or NULL (not yet invoiced in vTiger)
|
||||
- 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}
|
||||
|
||||
@ -716,12 +717,49 @@ class TimeTrackingVTigerService:
|
||||
|
||||
logger.info(f"✅ Total fetched: {len(all_timelogs)} Timelog entries from vTiger")
|
||||
|
||||
# We don't filter here - the existing code already filters by:
|
||||
# 1. duration > 0
|
||||
# 2. relatedto not empty
|
||||
# These filters happen in the processing loop below
|
||||
# Filter timelogs based on requirements:
|
||||
# 1. isbillable = '1' (only billable)
|
||||
# 2. cf_timelog_invoiced = '0' or NULL (not invoiced in vTiger)
|
||||
# 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...")
|
||||
|
||||
# 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']}"
|
||||
)
|
||||
|
||||
# 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
|
||||
logger.info(f"🔄 Updating time entry {approval.time_id} in database")
|
||||
update_query = """
|
||||
@ -185,6 +192,7 @@ class WizardService:
|
||||
approved_at = CURRENT_TIMESTAMP,
|
||||
approved_by = %s
|
||||
WHERE id = %s
|
||||
AND billed_via_thehub_id IS NULL
|
||||
"""
|
||||
|
||||
execute_update(
|
||||
@ -265,6 +273,13 @@ class WizardService:
|
||||
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_query = """
|
||||
UPDATE tmodule_times
|
||||
@ -274,6 +289,7 @@ class WizardService:
|
||||
approved_at = CURRENT_TIMESTAMP,
|
||||
approved_by = %s
|
||||
WHERE id = %s
|
||||
AND billed_via_thehub_id IS NULL
|
||||
"""
|
||||
|
||||
execute_update(update_query, (reason, user_id, time_id))
|
||||
@ -341,6 +357,13 @@ class WizardService:
|
||||
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
|
||||
update_query = """
|
||||
UPDATE tmodule_times
|
||||
@ -352,6 +375,7 @@ class WizardService:
|
||||
approved_at = NULL,
|
||||
approved_by = NULL
|
||||
WHERE id = %s
|
||||
AND billed_via_thehub_id IS NULL
|
||||
"""
|
||||
|
||||
execute_update(update_query, (reason, time_id))
|
||||
@ -490,7 +514,9 @@ class WizardService:
|
||||
SELECT c.id, c.title
|
||||
FROM tmodule_times t
|
||||
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
|
||||
LIMIT 1
|
||||
"""
|
||||
@ -558,6 +584,13 @@ class WizardService:
|
||||
AND t.status = 'pending'
|
||||
AND t.billable = true
|
||||
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
|
||||
ORDER BY t.worked_date, t.id
|
||||
"""
|
||||
@ -585,6 +618,13 @@ class WizardService:
|
||||
AND t.status = 'pending'
|
||||
AND t.billable = true
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
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