Compare commits
5 Commits
097f0633f5
...
bbb9ce8487
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbb9ce8487 | ||
|
|
8ac3a9db2f | ||
|
|
a867a7f128 | ||
|
|
0dd24c6420 | ||
|
|
d228362617 |
@ -101,7 +101,7 @@ async def get_contacts(
|
|||||||
{where_sql}
|
{where_sql}
|
||||||
GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile,
|
GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile,
|
||||||
c.title, c.department, c.is_active, c.created_at, c.updated_at
|
c.title, c.department, c.is_active, c.created_at, c.updated_at
|
||||||
ORDER BY c.first_name, c.last_name
|
ORDER BY company_count DESC, c.last_name, c.first_name
|
||||||
LIMIT %s OFFSET %s
|
LIMIT %s OFFSET %s
|
||||||
"""
|
"""
|
||||||
params.extend([limit, offset])
|
params.extend([limit, offset])
|
||||||
|
|||||||
@ -1,6 +1,33 @@
|
|||||||
"""
|
"""
|
||||||
System Sync Router
|
System Sync Router
|
||||||
API endpoints for syncing data between vTiger, e-conomic and Hub
|
API endpoints for syncing data between vTiger, e-conomic and Hub
|
||||||
|
|
||||||
|
SYNC ARCHITECTURE - Field Ownership:
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
E-CONOMIC owns and syncs:
|
||||||
|
- economic_customer_number (primary key from e-conomic)
|
||||||
|
- address, city, postal_code, country (physical address)
|
||||||
|
- email_domain, website (contact information)
|
||||||
|
- cvr_number (used for matching only, not overwritten if already set)
|
||||||
|
|
||||||
|
vTIGER owns and syncs:
|
||||||
|
- vtiger_id (primary key from vTiger)
|
||||||
|
- vtiger_account_no
|
||||||
|
- Contact records and contact-company relationships
|
||||||
|
|
||||||
|
HUB owns (manual or first-sync only):
|
||||||
|
- name (can be synced initially but not overwritten)
|
||||||
|
- cvr_number (used for matching, set once)
|
||||||
|
- Tags, notes, custom fields
|
||||||
|
|
||||||
|
SYNC RULES:
|
||||||
|
===========
|
||||||
|
1. NEVER overwrite source ID if already set (vtiger_id, economic_customer_number)
|
||||||
|
2. Match order: CVR → Source ID → Name (normalized)
|
||||||
|
3. Re-sync is idempotent - can run multiple times safely
|
||||||
|
4. Contact relationships are REPLACED on sync (not added)
|
||||||
|
5. Each sync only updates fields it owns
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -124,6 +151,7 @@ async def sync_from_vtiger() -> Dict[str, Any]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if current_vtiger_id is None:
|
if current_vtiger_id is None:
|
||||||
|
# Only set vtiger_id if it's currently NULL
|
||||||
execute_query(
|
execute_query(
|
||||||
"UPDATE customers SET vtiger_id = %s, last_synced_at = NOW() WHERE id = %s",
|
"UPDATE customers SET vtiger_id = %s, last_synced_at = NOW() WHERE id = %s",
|
||||||
(vtiger_id, existing[0]['id'])
|
(vtiger_id, existing[0]['id'])
|
||||||
@ -131,13 +159,10 @@ async def sync_from_vtiger() -> Dict[str, Any]:
|
|||||||
linked_count += 1
|
linked_count += 1
|
||||||
logger.info(f"🔗 Linket: {existing[0]['name']} → vTiger #{vtiger_id} (match: {match_method}, CVR: {cvr or 'ingen'})")
|
logger.info(f"🔗 Linket: {existing[0]['name']} → vTiger #{vtiger_id} (match: {match_method}, CVR: {cvr or 'ingen'})")
|
||||||
elif current_vtiger_id != vtiger_id:
|
elif current_vtiger_id != vtiger_id:
|
||||||
# Update if different vTiger ID
|
# SKIP if different vTiger ID - do NOT overwrite existing vtiger_id
|
||||||
execute_query(
|
logger.warning(f"⚠️ Springer over: {existing[0]['name']} har allerede vTiger #{current_vtiger_id}, vil ikke overskrive med #{vtiger_id}")
|
||||||
"UPDATE customers SET vtiger_id = %s, last_synced_at = NOW() WHERE id = %s",
|
not_found_count += 1
|
||||||
(vtiger_id, existing[0]['id'])
|
continue
|
||||||
)
|
|
||||||
updated_count += 1
|
|
||||||
logger.info(f"✏️ Opdateret vTiger ID: {existing[0]['name']} → {vtiger_id} (var: {current_vtiger_id})")
|
|
||||||
else:
|
else:
|
||||||
# Already linked, just update timestamp
|
# Already linked, just update timestamp
|
||||||
execute_query("UPDATE customers SET last_synced_at = NOW() WHERE id = %s", (existing[0]['id'],))
|
execute_query("UPDATE customers SET last_synced_at = NOW() WHERE id = %s", (existing[0]['id'],))
|
||||||
@ -386,19 +411,14 @@ async def sync_vtiger_contacts() -> Dict[str, Any]:
|
|||||||
customer_id = customer_result[0]['id']
|
customer_id = customer_result[0]['id']
|
||||||
customer_name = customer_result[0]['name']
|
customer_name = customer_result[0]['name']
|
||||||
|
|
||||||
# Check if link already exists
|
# DELETE existing links for this contact (we replace, not append)
|
||||||
existing_link = execute_query(
|
# This ensures re-sync updates links to match current vTiger state
|
||||||
"SELECT id FROM contact_companies WHERE contact_id = %s AND customer_id = %s",
|
execute_query(
|
||||||
(contact_id, customer_id)
|
"DELETE FROM contact_companies WHERE contact_id = %s",
|
||||||
|
(contact_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_link:
|
# CREATE new link from vTiger
|
||||||
# Already linked
|
|
||||||
if debug_count <= 20:
|
|
||||||
logger.warning(f" ↳ Already linked to '{customer_name}'")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# CREATE LINK
|
|
||||||
execute_query(
|
execute_query(
|
||||||
"INSERT INTO contact_companies (contact_id, customer_id, is_primary) VALUES (%s, %s, false)",
|
"INSERT INTO contact_companies (contact_id, customer_id, is_primary) VALUES (%s, %s, false)",
|
||||||
(contact_id, customer_id)
|
(contact_id, customer_id)
|
||||||
@ -502,13 +522,13 @@ async def sync_from_economic() -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Update existing customer (always sync economic_customer_number from e-conomic)
|
# Update existing customer - ONLY update fields e-conomic owns
|
||||||
|
# E-conomic does NOT overwrite: name, cvr_number (set once only)
|
||||||
update_query = """
|
update_query = """
|
||||||
UPDATE customers SET
|
UPDATE customers SET
|
||||||
name = %s,
|
|
||||||
economic_customer_number = %s,
|
economic_customer_number = %s,
|
||||||
cvr_number = %s,
|
|
||||||
email_domain = %s,
|
email_domain = %s,
|
||||||
|
address = %s,
|
||||||
city = %s,
|
city = %s,
|
||||||
postal_code = %s,
|
postal_code = %s,
|
||||||
country = %s,
|
country = %s,
|
||||||
@ -517,7 +537,7 @@ async def sync_from_economic() -> Dict[str, Any]:
|
|||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
"""
|
"""
|
||||||
execute_query(update_query, (
|
execute_query(update_query, (
|
||||||
name, customer_number, cvr, email_domain, city, zip_code, country, website, existing[0]['id']
|
customer_number, email_domain, address, city, zip_code, country, website, existing[0]['id']
|
||||||
))
|
))
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
logger.info(f"✏️ Opdateret: {name} (e-conomic #{customer_number}, CVR: {cvr or 'ingen'})")
|
logger.info(f"✏️ Opdateret: {name} (e-conomic #{customer_number}, CVR: {cvr or 'ingen'})")
|
||||||
@ -526,12 +546,12 @@ async def sync_from_economic() -> Dict[str, Any]:
|
|||||||
insert_query = """
|
insert_query = """
|
||||||
INSERT INTO customers
|
INSERT INTO customers
|
||||||
(name, economic_customer_number, cvr_number, email_domain,
|
(name, economic_customer_number, cvr_number, email_domain,
|
||||||
city, postal_code, country, website, last_synced_at)
|
address, city, postal_code, country, website, last_synced_at)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW())
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
|
||||||
RETURNING id
|
RETURNING id
|
||||||
"""
|
"""
|
||||||
result = execute_query(insert_query, (
|
result = execute_query(insert_query, (
|
||||||
name, customer_number, cvr, email_domain, city, zip_code, country, website
|
name, customer_number, cvr, email_domain, address, city, zip_code, country, website
|
||||||
))
|
))
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
|
|||||||
@ -84,7 +84,10 @@ class OrderService:
|
|||||||
(customer_id,))
|
(customer_id,))
|
||||||
|
|
||||||
if not customer:
|
if not customer:
|
||||||
raise HTTPException(status_code=404, detail="Customer not found")
|
raise HTTPException(status_code=404, detail=f"Customer {customer_id} not found in tmodule_customers")
|
||||||
|
|
||||||
|
# Debug log
|
||||||
|
logger.info(f"✅ Found customer: {customer.get('name') if isinstance(customer, dict) else type(customer)}")
|
||||||
|
|
||||||
# Hent godkendte tider for kunden med case og contact detaljer
|
# Hent godkendte tider for kunden med case og contact detaljer
|
||||||
query = """
|
query = """
|
||||||
|
|||||||
@ -118,6 +118,69 @@ async def test_vtiger_connection():
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/debug/raw-stats", tags=["Debug"])
|
||||||
|
async def get_debug_raw_stats():
|
||||||
|
"""
|
||||||
|
🔍 DEBUG: Vis rå statistik fra databasen uden filtering.
|
||||||
|
|
||||||
|
Bruges til at diagnosticere hvorfor timelogs ikke vises.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Total counts without any filtering
|
||||||
|
total_query = """
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM tmodule_customers) as total_customers,
|
||||||
|
(SELECT COUNT(*) FROM tmodule_cases) as total_cases,
|
||||||
|
(SELECT COUNT(*) FROM tmodule_times) as total_times,
|
||||||
|
(SELECT COUNT(*) FROM tmodule_times WHERE billable = true) as billable_times,
|
||||||
|
(SELECT COUNT(*) FROM tmodule_times WHERE status = 'pending') as pending_times,
|
||||||
|
(SELECT COUNT(*) FROM tmodule_times WHERE vtiger_data->>'cf_timelog_invoiced' = '0') as not_invoiced_times,
|
||||||
|
(SELECT COUNT(*) FROM tmodule_times WHERE vtiger_data->>'cf_timelog_invoiced' IS NULL) as null_invoiced_times,
|
||||||
|
(SELECT COUNT(*) FROM tmodule_times WHERE billable = true AND status = 'pending') as billable_pending,
|
||||||
|
(SELECT COUNT(*) FROM tmodule_times WHERE billable = true AND status = 'pending' AND vtiger_data->>'cf_timelog_invoiced' = '0') as filtered_pending
|
||||||
|
"""
|
||||||
|
totals = execute_query_single(total_query)
|
||||||
|
|
||||||
|
# Sample timelogs to see actual data
|
||||||
|
sample_query = """
|
||||||
|
SELECT
|
||||||
|
id, vtiger_id, description, worked_date, original_hours,
|
||||||
|
status, billable,
|
||||||
|
vtiger_data->>'cf_timelog_invoiced' as cf_timelog_invoiced,
|
||||||
|
vtiger_data->>'isbillable' as is_billable_field,
|
||||||
|
customer_id, case_id
|
||||||
|
FROM tmodule_times
|
||||||
|
ORDER BY worked_date DESC
|
||||||
|
LIMIT 10
|
||||||
|
"""
|
||||||
|
samples = execute_query(sample_query)
|
||||||
|
|
||||||
|
# Check what invoice statuses actually exist
|
||||||
|
invoice_status_query = """
|
||||||
|
SELECT
|
||||||
|
vtiger_data->>'cf_timelog_invoiced' as invoice_status,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM tmodule_times
|
||||||
|
GROUP BY vtiger_data->>'cf_timelog_invoiced'
|
||||||
|
ORDER BY count DESC
|
||||||
|
"""
|
||||||
|
invoice_statuses = execute_query(invoice_status_query)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"totals": totals,
|
||||||
|
"sample_timelogs": samples,
|
||||||
|
"invoice_statuses": invoice_statuses,
|
||||||
|
"explanation": {
|
||||||
|
"issue": "If not_invoiced_times is 0 but total_times > 0, then the cf_timelog_invoiced field is not '0' in vTiger",
|
||||||
|
"solution": "The SQL views filter on cf_timelog_invoiced = '0' but vTiger might use different values",
|
||||||
|
"check": "Look at invoice_statuses to see what values actually exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Debug query failed: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# WIZARD / APPROVAL ENDPOINTS
|
# WIZARD / APPROVAL ENDPOINTS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@ -567,8 +567,9 @@
|
|||||||
caseLink = 'Ingen case';
|
caseLink = 'Ingen case';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get hourly rate (customer rate or default)
|
// Get hourly rate (customer rate or default) - parse as float if string
|
||||||
const hourlyRate = e.customer_rate || currentEntry.customer_rate || defaultHourlyRate;
|
const rateValue = e.customer_rate || currentEntry.customer_rate || defaultHourlyRate;
|
||||||
|
const hourlyRate = typeof rateValue === 'string' ? parseFloat(rateValue) : rateValue;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="card time-entry-card mb-3" id="entry-card-${e.id}">
|
<div class="card time-entry-card mb-3" id="entry-card-${e.id}">
|
||||||
@ -804,7 +805,9 @@
|
|||||||
const response = await fetch(`/api/v1/timetracking/wizard/progress/${currentEntry.customer_id}`);
|
const response = await fetch(`/api/v1/timetracking/wizard/progress/${currentEntry.customer_id}`);
|
||||||
const progress = await response.json();
|
const progress = await response.json();
|
||||||
|
|
||||||
const hourlyRate = currentEntry.customer_rate || defaultHourlyRate;
|
// Parse hourly rate as number (may be string from DB)
|
||||||
|
const rateValue = currentEntry.customer_rate || defaultHourlyRate;
|
||||||
|
const hourlyRate = typeof rateValue === 'string' ? parseFloat(rateValue) : rateValue;
|
||||||
document.getElementById('context-hourly-rate').textContent = hourlyRate.toFixed(2) + ' DKK';
|
document.getElementById('context-hourly-rate').textContent = hourlyRate.toFixed(2) + ' DKK';
|
||||||
|
|
||||||
// Vis antal registreringer (vi har ikke timer-totaler i progress endpointet)
|
// Vis antal registreringer (vi har ikke timer-totaler i progress endpointet)
|
||||||
@ -1081,7 +1084,8 @@
|
|||||||
|
|
||||||
// Get billable hours and hourly rate from calculation
|
// Get billable hours and hourly rate from calculation
|
||||||
const billableHours = window.entryBillableHours?.[entryId] || entry.original_hours;
|
const billableHours = window.entryBillableHours?.[entryId] || entry.original_hours;
|
||||||
const hourlyRate = window.entryHourlyRates?.[entryId] || entry.customer_rate || defaultHourlyRate;
|
const rateValue = window.entryHourlyRates?.[entryId] || entry.customer_rate || defaultHourlyRate;
|
||||||
|
const hourlyRate = typeof rateValue === 'string' ? parseFloat(rateValue) : rateValue;
|
||||||
|
|
||||||
// Get travel checkbox state
|
// Get travel checkbox state
|
||||||
const travelCheckbox = document.getElementById(`travel-${entryId}`);
|
const travelCheckbox = document.getElementById(`travel-${entryId}`);
|
||||||
|
|||||||
53
deploy_to_prod.sh
Normal file
53
deploy_to_prod.sh
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Deploy BMC Hub to production server
|
||||||
|
# Usage: ./deploy_to_prod.sh v1.3.56
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
PROD_SERVER="bmcadmin@172.16.31.183"
|
||||||
|
PROD_DIR="/srv/podman/bmc_hub_v1.0"
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "❌ Usage: ./deploy_to_prod.sh v1.3.56"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🚀 Deploying $VERSION to production..."
|
||||||
|
echo "======================================="
|
||||||
|
|
||||||
|
# Update .env file
|
||||||
|
echo "📝 Updating RELEASE_VERSION to $VERSION..."
|
||||||
|
ssh $PROD_SERVER "sed -i 's/^RELEASE_VERSION=.*/RELEASE_VERSION=$VERSION/' $PROD_DIR/.env"
|
||||||
|
|
||||||
|
# Stop containers
|
||||||
|
echo "🛑 Stopping containers..."
|
||||||
|
ssh $PROD_SERVER "cd $PROD_DIR && podman-compose down"
|
||||||
|
|
||||||
|
# Rebuild with no cache (to pull latest version from Gitea)
|
||||||
|
echo "🔨 Building new image from version $VERSION..."
|
||||||
|
ssh $PROD_SERVER "cd $PROD_DIR && podman-compose build --no-cache"
|
||||||
|
|
||||||
|
# Start containers
|
||||||
|
echo "▶️ Starting containers..."
|
||||||
|
ssh $PROD_SERVER "cd $PROD_DIR && podman-compose up -d"
|
||||||
|
|
||||||
|
# Wait for service to be ready
|
||||||
|
echo "⏳ Waiting for service to start..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
# Check health
|
||||||
|
echo "🏥 Checking service health..."
|
||||||
|
HEALTH=$(curl -s http://172.16.31.183:8000/health || echo "FAILED")
|
||||||
|
if echo "$HEALTH" | grep -q "ok"; then
|
||||||
|
echo "✅ Deployment successful!"
|
||||||
|
echo "$HEALTH"
|
||||||
|
else
|
||||||
|
echo "❌ Health check failed:"
|
||||||
|
echo "$HEALTH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Production is now running version $VERSION"
|
||||||
|
echo " http://172.16.31.183:8000"
|
||||||
@ -42,6 +42,7 @@ services:
|
|||||||
# Mount for local development - live code reload
|
# Mount for local development - live code reload
|
||||||
- ./app:/app/app:ro
|
- ./app:/app/app:ro
|
||||||
- ./main.py:/app/main.py:ro
|
- ./main.py:/app/main.py:ro
|
||||||
|
- ./VERSION:/app/VERSION:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
9
main.py
9
main.py
@ -4,6 +4,7 @@ Main application entry point
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
@ -13,6 +14,14 @@ from contextlib import asynccontextmanager
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import init_db
|
from app.core.database import init_db
|
||||||
|
|
||||||
|
def get_version():
|
||||||
|
"""Read version from VERSION file"""
|
||||||
|
try:
|
||||||
|
version_file = Path(__file__).parent / "VERSION"
|
||||||
|
return version_file.read_text().strip()
|
||||||
|
except Exception:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
# Import Feature Routers
|
# Import Feature Routers
|
||||||
from app.customers.backend import router as customers_api
|
from app.customers.backend import router as customers_api
|
||||||
from app.customers.backend import views as customers_views
|
from app.customers.backend import views as customers_views
|
||||||
|
|||||||
20
test_vtiger_accounts.py
Normal file
20
test_vtiger_accounts.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/Users/christianthomas/DEV/bmc_hub_dev')
|
||||||
|
from app.services.vtiger_service import VTigerService
|
||||||
|
|
||||||
|
async def test_accounts():
|
||||||
|
vtiger = VTigerService()
|
||||||
|
|
||||||
|
# Check what 3x760 and 3x811 are in vTiger
|
||||||
|
for vtiger_id in ['3x760', '3x811']:
|
||||||
|
query = f"SELECT id, accountname FROM Accounts WHERE id = '{vtiger_id}';"
|
||||||
|
result = await vtiger.query(query)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print(f"{vtiger_id} = {result[0].get('accountname')}")
|
||||||
|
else:
|
||||||
|
print(f"{vtiger_id} = NOT FOUND")
|
||||||
|
|
||||||
|
asyncio.run(test_accounts())
|
||||||
44
test_vtiger_contact.py
Normal file
44
test_vtiger_contact.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to check vTiger contact data
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/Users/christianthomas/DEV/bmc_hub_dev')
|
||||||
|
|
||||||
|
from app.services.vtiger_service import VTigerService
|
||||||
|
|
||||||
|
async def test_contact():
|
||||||
|
vtiger = VTigerService()
|
||||||
|
|
||||||
|
# Fetch contact by email
|
||||||
|
query = "SELECT id, firstname, lastname, email, account_id FROM Contacts WHERE email = 'accounting@arbodania.com';"
|
||||||
|
print(f"Query: {query}")
|
||||||
|
|
||||||
|
result = await vtiger.query(query)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
contact = result[0]
|
||||||
|
print(f"\n✅ Contact found:")
|
||||||
|
print(f" ID: {contact.get('id')}")
|
||||||
|
print(f" Name: {contact.get('firstname')} {contact.get('lastname')}")
|
||||||
|
print(f" Email: {contact.get('email')}")
|
||||||
|
print(f" Account ID: {contact.get('account_id')}")
|
||||||
|
|
||||||
|
# Now fetch the account details
|
||||||
|
account_id = contact.get('account_id')
|
||||||
|
if account_id:
|
||||||
|
account_query = f"SELECT id, accountname FROM Accounts WHERE id = '{account_id}';"
|
||||||
|
print(f"\nFetching account: {account_query}")
|
||||||
|
account_result = await vtiger.query(account_query)
|
||||||
|
|
||||||
|
if account_result:
|
||||||
|
account = account_result[0]
|
||||||
|
print(f"\n✅ Account found:")
|
||||||
|
print(f" ID: {account.get('id')}")
|
||||||
|
print(f" Name: {account.get('accountname')}")
|
||||||
|
else:
|
||||||
|
print("❌ Contact not found")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_contact())
|
||||||
Loading…
Reference in New Issue
Block a user