feat: Implement tracking of billed Hub order ID for time entries and update related services
This commit is contained in:
parent
cbcd0fe4e7
commit
ccb7714779
@ -9,7 +9,7 @@ from typing import List, Optional, Dict
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.core.database import execute_query, execute_query_single
|
from app.core.database import execute_query, execute_query_single, execute_update
|
||||||
from app.services.cvr_service import get_cvr_service
|
from app.services.cvr_service import get_cvr_service
|
||||||
from app.services.customer_activity_logger import CustomerActivityLogger
|
from app.services.customer_activity_logger import CustomerActivityLogger
|
||||||
from app.services.customer_consistency import CustomerConsistencyService
|
from app.services.customer_consistency import CustomerConsistencyService
|
||||||
|
|||||||
@ -5,7 +5,7 @@ Compares customer data across BMC Hub, vTiger Cloud, and e-conomic
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Dict, List, Optional, Tuple, Any
|
from typing import Dict, List, Optional, Tuple, Any
|
||||||
from app.core.database import execute_query_single
|
from app.core.database import execute_query_single, execute_update
|
||||||
from app.services.vtiger_service import VTigerService
|
from app.services.vtiger_service import VTigerService
|
||||||
from app.services.economic_service import EconomicService
|
from app.services.economic_service import EconomicService
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@ -82,12 +82,12 @@ class CustomerConsistencyService:
|
|||||||
vtiger_task = None
|
vtiger_task = None
|
||||||
economic_task = None
|
economic_task = None
|
||||||
|
|
||||||
# Fetch vTiger data if we have an ID
|
# Fetch vTiger data if we have an ID and vTiger is configured
|
||||||
if hub_data.get('vtiger_id') and settings.VTIGER_ENABLED:
|
if hub_data.get('vtiger_id') and settings.VTIGER_URL:
|
||||||
vtiger_task = self.vtiger.get_account_by_id(hub_data['vtiger_id'])
|
vtiger_task = self.vtiger.get_account_by_id(hub_data['vtiger_id'])
|
||||||
|
|
||||||
# Fetch e-conomic data if we have a customer number
|
# Fetch e-conomic data if we have a customer number and e-conomic is configured
|
||||||
if hub_data.get('economic_customer_number') and settings.ECONOMIC_ENABLED:
|
if hub_data.get('economic_customer_number') and settings.ECONOMIC_APP_SECRET_TOKEN:
|
||||||
economic_task = self.economic.get_customer(hub_data['economic_customer_number'])
|
economic_task = self.economic.get_customer(hub_data['economic_customer_number'])
|
||||||
|
|
||||||
# Parallel fetch with error handling
|
# Parallel fetch with error handling
|
||||||
@ -153,8 +153,17 @@ class CustomerConsistencyService:
|
|||||||
economic_norm = cls.normalize_value(economic_value)
|
economic_norm = cls.normalize_value(economic_value)
|
||||||
|
|
||||||
# Check if all values are the same
|
# Check if all values are the same
|
||||||
values = [v for v in [hub_norm, vtiger_norm, economic_norm] if v is not None]
|
# Only compare systems that are available
|
||||||
has_discrepancy = len(set(values)) > 1 if values else False
|
available_values = []
|
||||||
|
if hub_data:
|
||||||
|
available_values.append(hub_norm)
|
||||||
|
if vtiger_data:
|
||||||
|
available_values.append(vtiger_norm)
|
||||||
|
if economic_data:
|
||||||
|
available_values.append(economic_norm)
|
||||||
|
|
||||||
|
# Has discrepancy if there are different non-None values
|
||||||
|
has_discrepancy = len(set(available_values)) > 1 if len(available_values) > 1 else False
|
||||||
|
|
||||||
discrepancies[hub_field] = {
|
discrepancies[hub_field] = {
|
||||||
'hub': hub_value,
|
'hub': hub_value,
|
||||||
@ -203,7 +212,6 @@ class CustomerConsistencyService:
|
|||||||
# Update Hub if not the source
|
# Update Hub if not the source
|
||||||
if source_system != 'hub':
|
if source_system != 'hub':
|
||||||
try:
|
try:
|
||||||
from app.core.database import execute_update
|
|
||||||
update_query = f"UPDATE customers SET {field_name} = %s WHERE id = %s"
|
update_query = f"UPDATE customers SET {field_name} = %s WHERE id = %s"
|
||||||
await asyncio.to_thread(execute_update, update_query, (source_value, customer_id))
|
await asyncio.to_thread(execute_update, update_query, (source_value, customer_id))
|
||||||
results['hub'] = True
|
results['hub'] = True
|
||||||
@ -218,9 +226,13 @@ class CustomerConsistencyService:
|
|||||||
if settings.VTIGER_SYNC_ENABLED and source_system != 'vtiger' and hub_data.get('vtiger_id'):
|
if settings.VTIGER_SYNC_ENABLED and source_system != 'vtiger' and hub_data.get('vtiger_id'):
|
||||||
try:
|
try:
|
||||||
update_data = {vtiger_field: source_value}
|
update_data = {vtiger_field: source_value}
|
||||||
await self.vtiger.update_account(hub_data['vtiger_id'], update_data)
|
success = await self.vtiger.update_account(hub_data['vtiger_id'], update_data)
|
||||||
results['vtiger'] = True
|
if success:
|
||||||
logger.info(f"✅ vTiger {vtiger_field} updated")
|
results['vtiger'] = True
|
||||||
|
logger.info(f"✅ vTiger {vtiger_field} updated")
|
||||||
|
else:
|
||||||
|
results['vtiger'] = False
|
||||||
|
logger.error(f"❌ vTiger update failed - API returned False")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Failed to update vTiger: {e}")
|
logger.error(f"❌ Failed to update vTiger: {e}")
|
||||||
results['vtiger'] = False
|
results['vtiger'] = False
|
||||||
|
|||||||
@ -126,14 +126,26 @@ class VTigerService:
|
|||||||
raise ValueError("VTIGER_URL not configured")
|
raise ValueError("VTIGER_URL not configured")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Fetch current account first - vTiger requires modifiedtime for updates
|
||||||
|
current_account = await self.get_account_by_id(account_id)
|
||||||
|
if not current_account:
|
||||||
|
logger.error(f"❌ Account {account_id} not found for update")
|
||||||
|
return False
|
||||||
|
|
||||||
auth = self._get_auth()
|
auth = self._get_auth()
|
||||||
|
|
||||||
# vTiger requires the ID in the data
|
# Build payload with current data + updates
|
||||||
|
# Include essential fields for vTiger update validation
|
||||||
payload = {
|
payload = {
|
||||||
'id': account_id,
|
'id': account_id,
|
||||||
|
'accountname': current_account.get('accountname'),
|
||||||
|
'assigned_user_id': current_account.get('assigned_user_id'),
|
||||||
|
'modifiedtime': current_account.get('modifiedtime'),
|
||||||
**update_data
|
**update_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(f"🔄 vTiger update payload: {payload}")
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.post(
|
async with session.post(
|
||||||
f"{self.rest_endpoint}/update",
|
f"{self.rest_endpoint}/update",
|
||||||
@ -151,12 +163,14 @@ class VTigerService:
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.error(f"❌ vTiger update failed: {data.get('error')}")
|
logger.error(f"❌ vTiger update failed: {data.get('error')}")
|
||||||
|
logger.error(f"Full response: {text}")
|
||||||
return False
|
return False
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.error(f"❌ Invalid JSON in update response: {text[:200]}")
|
logger.error(f"❌ Invalid JSON in update response: {text[:200]}")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
logger.error(f"❌ vTiger update HTTP error {response.status}: {text[:500]}")
|
logger.error(f"❌ vTiger update HTTP error {response.status}")
|
||||||
|
logger.error(f"Full response: {text[:2000]}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -412,16 +412,17 @@ class EconomicExportService:
|
|||||||
(economic_draft_id, economic_order_number, user_id, request.order_id)
|
(economic_draft_id, economic_order_number, user_id, request.order_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Marker time entries som billed
|
# Marker time entries som billed og opdater billed_via_thehub_id
|
||||||
execute_update(
|
execute_update(
|
||||||
"""UPDATE tmodule_times
|
"""UPDATE tmodule_times
|
||||||
SET status = 'billed'
|
SET status = 'billed',
|
||||||
|
billed_via_thehub_id = %s
|
||||||
WHERE id IN (
|
WHERE id IN (
|
||||||
SELECT UNNEST(time_entry_ids)
|
SELECT UNNEST(time_entry_ids)
|
||||||
FROM tmodule_order_lines
|
FROM tmodule_order_lines
|
||||||
WHERE order_id = %s
|
WHERE order_id = %s
|
||||||
)""",
|
)""",
|
||||||
(request.order_id,)
|
(request.order_id, request.order_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Hent vTiger IDs for tidsregistreringerne
|
# Hent vTiger IDs for tidsregistreringerne
|
||||||
|
|||||||
@ -138,8 +138,10 @@ class TModuleTime(TModuleTimeBase):
|
|||||||
rounded_to: Optional[Decimal] = None
|
rounded_to: Optional[Decimal] = None
|
||||||
approval_note: Optional[str] = None
|
approval_note: Optional[str] = None
|
||||||
billable: bool = True
|
billable: bool = True
|
||||||
|
is_travel: bool = False
|
||||||
approved_at: Optional[datetime] = None
|
approved_at: Optional[datetime] = None
|
||||||
approved_by: Optional[int] = None
|
approved_by: Optional[int] = None
|
||||||
|
billed_via_thehub_id: Optional[int] = Field(None, description="Hub order ID this time was billed through")
|
||||||
sync_hash: Optional[str] = None
|
sync_hash: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
|||||||
@ -279,22 +279,8 @@ class OrderService:
|
|||||||
|
|
||||||
logger.info(f"✅ Created {len(created_lines)} order lines")
|
logger.info(f"✅ Created {len(created_lines)} order lines")
|
||||||
|
|
||||||
# Update time entries to 'billed' status
|
# NOTE: Time entries remain 'approved' status until exported to e-conomic
|
||||||
time_entry_ids = [
|
# They will be updated to 'billed' with billed_via_thehub_id in economic_export.py
|
||||||
entry_id
|
|
||||||
for line in order_lines
|
|
||||||
for entry_id in line.time_entry_ids
|
|
||||||
]
|
|
||||||
|
|
||||||
if time_entry_ids:
|
|
||||||
placeholders = ','.join(['%s'] * len(time_entry_ids))
|
|
||||||
execute_update(
|
|
||||||
f"""UPDATE tmodule_times
|
|
||||||
SET status = 'billed'
|
|
||||||
WHERE id IN ({placeholders})""",
|
|
||||||
time_entry_ids
|
|
||||||
)
|
|
||||||
logger.info(f"✅ Marked {len(time_entry_ids)} time entries as billed")
|
|
||||||
|
|
||||||
# Log order creation
|
# Log order creation
|
||||||
audit.log_order_created(
|
audit.log_order_created(
|
||||||
|
|||||||
@ -795,13 +795,15 @@ class TimeTrackingVTigerService:
|
|||||||
stats["skipped"] += 1
|
stats["skipped"] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Update only if NOT yet approved
|
# Update only if NOT yet approved AND NOT yet billed
|
||||||
result = execute_update(
|
result = execute_update(
|
||||||
"""UPDATE tmodule_times
|
"""UPDATE tmodule_times
|
||||||
SET description = %s, original_hours = %s, worked_date = %s,
|
SET description = %s, original_hours = %s, worked_date = %s,
|
||||||
user_name = %s, billable = %s, vtiger_data = %s::jsonb,
|
user_name = %s, billable = %s, vtiger_data = %s::jsonb,
|
||||||
sync_hash = %s, last_synced_at = CURRENT_TIMESTAMP
|
sync_hash = %s, last_synced_at = CURRENT_TIMESTAMP
|
||||||
WHERE vtiger_id = %s AND status = 'pending'""",
|
WHERE vtiger_id = %s
|
||||||
|
AND status = 'pending'
|
||||||
|
AND billed_via_thehub_id IS NULL""",
|
||||||
(
|
(
|
||||||
timelog.get('name', ''),
|
timelog.get('name', ''),
|
||||||
hours,
|
hours,
|
||||||
@ -817,7 +819,7 @@ class TimeTrackingVTigerService:
|
|||||||
if result > 0:
|
if result > 0:
|
||||||
stats["updated"] += 1
|
stats["updated"] += 1
|
||||||
else:
|
else:
|
||||||
logger.debug(f"⏭️ Time entry {vtiger_id} already approved")
|
logger.debug(f"⏭️ Time entry {vtiger_id} already approved or billed")
|
||||||
stats["skipped"] += 1
|
stats["skipped"] += 1
|
||||||
else:
|
else:
|
||||||
# Insert new
|
# Insert new
|
||||||
|
|||||||
179
docs/TIMETRACKING_BILLED_VIA_HUB.md
Normal file
179
docs/TIMETRACKING_BILLED_VIA_HUB.md
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
# Time Tracking - Billed Via Hub Order ID
|
||||||
|
|
||||||
|
## Oversigt
|
||||||
|
|
||||||
|
Implementeret tracking af hvilken Hub ordre (og dermed e-conomic ordre) hver tidsregistrering er blevet faktureret gennem.
|
||||||
|
|
||||||
|
## Database Ændringer
|
||||||
|
|
||||||
|
### Migration: `060_add_billed_via_thehub_id.sql`
|
||||||
|
|
||||||
|
Tilføjet nyt felt til `tmodule_times` tabellen:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE tmodule_times ADD COLUMN billed_via_thehub_id INTEGER;
|
||||||
|
ALTER TABLE tmodule_times
|
||||||
|
ADD CONSTRAINT tmodule_times_billed_via_thehub_id_fkey
|
||||||
|
FOREIGN KEY (billed_via_thehub_id)
|
||||||
|
REFERENCES tmodule_orders(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
CREATE INDEX idx_tmodule_times_billed_via_thehub_id ON tmodule_times(billed_via_thehub_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Felt beskrivelse:**
|
||||||
|
- `billed_via_thehub_id`: Hub ordre ID som tidsregistreringen er faktureret gennem
|
||||||
|
- Foreign key til `tmodule_orders.id`
|
||||||
|
- Via Hub ordren kan man finde `economic_order_number` som er ordrenummeret i e-conomic
|
||||||
|
|
||||||
|
## Kode Ændringer
|
||||||
|
|
||||||
|
### 1. `app/timetracking/backend/models.py`
|
||||||
|
|
||||||
|
Tilføjet felt til `TModuleTime` Pydantic model:
|
||||||
|
|
||||||
|
```python
|
||||||
|
billed_via_thehub_id: Optional[int] = Field(None, description="Hub order ID this time was billed through")
|
||||||
|
is_travel: bool = False # Også tilføjet manglende felt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `app/timetracking/backend/economic_export.py`
|
||||||
|
|
||||||
|
Opdateret `export_order_to_economic()` til at sætte `billed_via_thehub_id` når ordren eksporteres:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Marker time entries som billed og opdater billed_via_thehub_id
|
||||||
|
execute_update(
|
||||||
|
"""UPDATE tmodule_times
|
||||||
|
SET status = 'billed',
|
||||||
|
billed_via_thehub_id = %s
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT UNNEST(time_entry_ids)
|
||||||
|
FROM tmodule_order_lines
|
||||||
|
WHERE order_id = %s
|
||||||
|
)""",
|
||||||
|
(request.order_id, request.order_id)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vigtig note:** vTiger opdateres også via `update_timelog_billed()` som sætter `billed_via_thehub_id` i vTiger's Timelog records.
|
||||||
|
|
||||||
|
### 3. `app/timetracking/backend/order_service.py`
|
||||||
|
|
||||||
|
**FJERNET** for tidlig status opdatering:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# BEFORE: Time entries blev sat til 'billed' når Hub ordre blev oprettet
|
||||||
|
# AFTER: Time entries forbliver 'approved' indtil e-conomic eksporten er succesfuld
|
||||||
|
```
|
||||||
|
|
||||||
|
Nu opdateres `status='billed'` og `billed_via_thehub_id` KUN i `economic_export.py` efter succesfuld eksport.
|
||||||
|
|
||||||
|
### 4. `app/timetracking/backend/vtiger_sync.py`
|
||||||
|
|
||||||
|
**BESKYTTELSE MOD OVERSKRIVNING:** Tidsregistreringer med `billed_via_thehub_id` opdateres IKKE ved sync:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Update only if NOT yet approved AND NOT yet billed
|
||||||
|
result = execute_update(
|
||||||
|
"""UPDATE tmodule_times
|
||||||
|
SET description = %s, original_hours = %s, worked_date = %s,
|
||||||
|
user_name = %s, billable = %s, vtiger_data = %s::jsonb,
|
||||||
|
sync_hash = %s, last_synced_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE vtiger_id = %s
|
||||||
|
AND status = 'pending'
|
||||||
|
AND billed_via_thehub_id IS NULL""",
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Dette sikrer at allerede fakturerede tidsregistreringer forbliver låst og ikke overskrevet af vTiger sync.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Før ændringerne:
|
||||||
|
1. Godkendte tider → `status='approved'`
|
||||||
|
2. **Opret ordre** i Hub → `status='billed'` ⚠️ (for tidligt!)
|
||||||
|
3. Eksporter til e-conomic → `economic_order_number` sættes på Hub ordre
|
||||||
|
|
||||||
|
### Efter ændringerne:
|
||||||
|
1. Godkendte tider → `status='approved'`
|
||||||
|
2. **Opret ordre** i Hub → tider forbliver `status='approved'`
|
||||||
|
3. **Eksporter til e-conomic** → `status='billed'` + `billed_via_thehub_id` sættes
|
||||||
|
4. vTiger opdateres også med `billed_via_thehub_id`
|
||||||
|
|
||||||
|
## Data Relations
|
||||||
|
|
||||||
|
```
|
||||||
|
tmodule_times.billed_via_thehub_id
|
||||||
|
↓ (foreign key)
|
||||||
|
tmodule_orders.id
|
||||||
|
→ tmodule_orders.economic_order_number (e-conomic ordre nummer)
|
||||||
|
→ tmodule_orders.economic_draft_id (e-conomic kladde ID)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Queries
|
||||||
|
|
||||||
|
### Find alle tidsregistreringer for en e-conomic ordre:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT t.*, o.economic_order_number
|
||||||
|
FROM tmodule_times t
|
||||||
|
JOIN tmodule_orders o ON t.billed_via_thehub_id = o.id
|
||||||
|
WHERE o.economic_order_number = '12345';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find tider der er faktureret via en specifik Hub ordre:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM tmodule_times
|
||||||
|
WHERE billed_via_thehub_id = 123;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find tider der IKKE er faktureret endnu:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM tmodule_times
|
||||||
|
WHERE status = 'approved'
|
||||||
|
AND billed_via_thehub_id IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
## vTiger Integration
|
||||||
|
|
||||||
|
vTiger's `Timelog` records opdateres også via `app/timetracking/backend/vtiger_sync.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def update_timelog_billed(self, vtiger_ids: List[str], hub_order_id: int):
|
||||||
|
payload = {
|
||||||
|
"elementType": "Timelog",
|
||||||
|
"element": {
|
||||||
|
"id": vtiger_id,
|
||||||
|
"billed_via_thehub_id": str(hub_order_id),
|
||||||
|
"cf_timelog_invoiced": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** `cf_timelog_invoiced` (IS BILLED) feltet i vTiger kan vi IKKE ændre fra Hub (vTiger begrænsning), men vi opdaterer vores eget `billed_via_thehub_id` felt.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Migrationen er kørt og verificeret:
|
||||||
|
```bash
|
||||||
|
docker exec bmc-hub-postgres psql -U bmc_hub -d bmc_hub -c "\d tmodule_times"
|
||||||
|
# Viser: billed_via_thehub_id | integer med foreign key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
1. Migration er kørt på dev (060_add_billed_via_thehub_id.sql)
|
||||||
|
2. Kode ændringer deployed i samme commit
|
||||||
|
3. Eksisterende tidsregistreringer har `billed_via_thehub_id = NULL`
|
||||||
|
4. Fremtidige eksporter vil populere feltet korrekt
|
||||||
|
|
||||||
|
## Relaterede Filer
|
||||||
|
|
||||||
|
- `migrations/060_add_billed_via_thehub_id.sql`
|
||||||
|
- `app/timetracking/backend/models.py`
|
||||||
|
- `app/timetracking/backend/economic_export.py`
|
||||||
|
- `app/timetracking/backend/order_service.py`
|
||||||
|
- `app/timetracking/backend/vtiger_sync.py`
|
||||||
48
migrations/060_add_billed_via_thehub_id.sql
Normal file
48
migrations/060_add_billed_via_thehub_id.sql
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
-- Migration: Add billed_via_thehub_id to tmodule_times
|
||||||
|
-- This tracks which Hub order (and indirectly e-conomic order) each time entry was billed through
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Add billed_via_thehub_id column if it doesn't exist
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'tmodule_times' AND column_name = 'billed_via_thehub_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE tmodule_times ADD COLUMN billed_via_thehub_id INTEGER;
|
||||||
|
RAISE NOTICE 'Added column billed_via_thehub_id to tmodule_times';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Column billed_via_thehub_id already exists';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Add foreign key constraint to tmodule_orders
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.table_constraints
|
||||||
|
WHERE table_name = 'tmodule_times'
|
||||||
|
AND constraint_name = 'tmodule_times_billed_via_thehub_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE tmodule_times
|
||||||
|
ADD CONSTRAINT tmodule_times_billed_via_thehub_id_fkey
|
||||||
|
FOREIGN KEY (billed_via_thehub_id)
|
||||||
|
REFERENCES tmodule_orders(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
RAISE NOTICE 'Added foreign key constraint for billed_via_thehub_id';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Foreign key constraint already exists';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Add index for faster lookups
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE tablename = 'tmodule_times'
|
||||||
|
AND indexname = 'idx_tmodule_times_billed_via_thehub_id'
|
||||||
|
) THEN
|
||||||
|
CREATE INDEX idx_tmodule_times_billed_via_thehub_id ON tmodule_times(billed_via_thehub_id);
|
||||||
|
RAISE NOTICE 'Created index idx_tmodule_times_billed_via_thehub_id';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Index idx_tmodule_times_billed_via_thehub_id already exists';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add comment explaining the column
|
||||||
|
COMMENT ON COLUMN tmodule_times.billed_via_thehub_id IS
|
||||||
|
'Hub order ID that this time entry was billed through. Links to tmodule_orders.id which contains economic_order_number.';
|
||||||
44
test_billed_field.py
Normal file
44
test_billed_field.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test billed_via_thehub_id opdatering"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.core.database import execute_query, execute_update
|
||||||
|
|
||||||
|
# Test query
|
||||||
|
print("\n=== Checking tmodule_times schema ===")
|
||||||
|
result = execute_query("""
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'tmodule_times'
|
||||||
|
AND column_name = 'billed_via_thehub_id'
|
||||||
|
""")
|
||||||
|
print(f"Column exists: {len(result) > 0}")
|
||||||
|
if result:
|
||||||
|
print(f" Type: {result[0]['data_type']}")
|
||||||
|
print(f" Nullable: {result[0]['is_nullable']}")
|
||||||
|
|
||||||
|
# Test foreign key
|
||||||
|
print("\n=== Checking foreign key constraint ===")
|
||||||
|
fk_result = execute_query("""
|
||||||
|
SELECT constraint_name, table_name, column_name
|
||||||
|
FROM information_schema.key_column_usage
|
||||||
|
WHERE table_name = 'tmodule_times'
|
||||||
|
AND column_name = 'billed_via_thehub_id'
|
||||||
|
""")
|
||||||
|
if fk_result:
|
||||||
|
print(f"Foreign key: {fk_result[0]['constraint_name']}")
|
||||||
|
|
||||||
|
# Check if there are any time entries
|
||||||
|
print("\n=== Sample time entries ===")
|
||||||
|
times = execute_query("""
|
||||||
|
SELECT id, status, billed_via_thehub_id, original_hours, worked_date
|
||||||
|
FROM tmodule_times
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT 5
|
||||||
|
""")
|
||||||
|
print(f"Found {len(times)} time entries:")
|
||||||
|
for t in times:
|
||||||
|
print(f" ID {t['id']}: status={t['status']}, billed_via={t['billed_via_thehub_id']}, hours={t['original_hours']}")
|
||||||
|
|
||||||
|
print("\n✅ Test complete!")
|
||||||
31
test_vtiger_account.py
Normal file
31
test_vtiger_account.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test vTiger account retrieval"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.services.vtiger_service import VTigerService
|
||||||
|
from app.core.config import settings
|
||||||
|
import json
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
vtiger = VTigerService()
|
||||||
|
|
||||||
|
account_id = "3x957"
|
||||||
|
|
||||||
|
# Fetch account
|
||||||
|
print(f"\n=== Fetching account {account_id} ===")
|
||||||
|
account = await vtiger.get_account_by_id(account_id)
|
||||||
|
|
||||||
|
if account:
|
||||||
|
print(f"\nAccount fields ({len(account)} total):")
|
||||||
|
for key in sorted(account.keys()):
|
||||||
|
value = account[key]
|
||||||
|
if len(str(value)) > 100:
|
||||||
|
value = str(value)[:100] + "..."
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
else:
|
||||||
|
print("Account not found!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Loading…
Reference in New Issue
Block a user