Hardware
@@ -324,18 +393,41 @@
const customerId = parseInt(window.location.pathname.split('/').pop());
let customerData = null;
+let eventListenersAdded = false;
+
document.addEventListener('DOMContentLoaded', () => {
+ if (eventListenersAdded) {
+ console.log('Event listeners already added, skipping...');
+ return;
+ }
+
loadCustomer();
// Load contacts when tab is shown
- document.querySelector('a[href="#contacts"]').addEventListener('shown.bs.tab', () => {
- loadContacts();
- });
+ const contactsTab = document.querySelector('a[href="#contacts"]');
+ if (contactsTab) {
+ contactsTab.addEventListener('shown.bs.tab', () => {
+ loadContacts();
+ }, { once: false });
+ }
+
+ // Load subscriptions when tab is shown
+ const subscriptionsTab = document.querySelector('a[href="#subscriptions"]');
+ if (subscriptionsTab) {
+ subscriptionsTab.addEventListener('shown.bs.tab', () => {
+ loadSubscriptions();
+ }, { once: false });
+ }
// Load activity when tab is shown
- document.querySelector('a[href="#activity"]').addEventListener('shown.bs.tab', () => {
- loadActivity();
- });
+ const activityTab = document.querySelector('a[href="#activity"]');
+ if (activityTab) {
+ activityTab.addEventListener('shown.bs.tab', () => {
+ loadActivity();
+ }, { once: false });
+ }
+
+ eventListenersAdded = true;
});
async function loadCustomer() {
@@ -458,6 +550,487 @@ async function loadContacts() {
}
}
+let subscriptionsLoaded = false;
+
+async function loadSubscriptions() {
+ const container = document.getElementById('subscriptionsContainer');
+
+ // Prevent duplicate loads
+ if (subscriptionsLoaded && container.innerHTML.includes('row g-4')) {
+ console.log('Subscriptions already loaded, skipping...');
+ return;
+ }
+
+ container.innerHTML = '
Henter data fra vTiger...
';
+
+ try {
+ const response = await fetch(`/api/v1/customers/${customerId}/subscriptions`);
+ const data = await response.json();
+
+ console.log('Loaded subscriptions:', data);
+
+ if (!response.ok) {
+ throw new Error(data.detail || 'Failed to load subscriptions');
+ }
+
+ if (data.status === 'no_vtiger_link') {
+ container.innerHTML = `
+
+
+ ${data.message}
+
+ `;
+ return;
+ }
+
+ displaySubscriptions(data);
+ subscriptionsLoaded = true;
+ } catch (error) {
+ console.error('Failed to load subscriptions:', error);
+ container.innerHTML = '
Kunne ikke indlæse abonnementer
';
+ }
+}
+
+function displaySubscriptions(data) {
+ const container = document.getElementById('subscriptionsContainer');
+ const { recurring_orders, sales_orders, subscriptions, expired_subscriptions, bmc_office_subscriptions } = data;
+
+ const totalItems = (sales_orders?.length || 0) + (subscriptions?.length || 0) + (bmc_office_subscriptions?.length || 0);
+
+ if (totalItems === 0) {
+ container.innerHTML = '
Ingen abonnementer eller salgsordre fundet
';
+ return;
+ }
+
+ // Create 3-column layout
+ let html = '
';
+
+ // Column 1: vTiger Subscriptions
+ html += `
+
+
+
+
+
+ vTiger Abonnementer
+
+ Fra Simply-CRM
+
+ ${renderSubscriptionsList(subscriptions || [])}
+
+
+ `;
+
+ // Column 2: Sales Orders
+ html += `
+
+
+
+
+
+ Γ
bne Salgsordre
+
+ Fra Simply-CRM
+
+ ${renderSalesOrdersList(sales_orders || [])}
+
+
+ `;
+
+ // Column 3: BMC Office Subscriptions
+ html += `
+
+
+
+
+
+ BMC Office Abonnementer
+
+ Fra lokalt system
+
+ ${renderBmcOfficeSubscriptionsList(bmc_office_subscriptions || [])}
+
+
+ `;
+
+ html += '
';
+
+ // Add comparison stats at bottom
+ const subTotal = subscriptions?.reduce((sum, sub) => sum + parseFloat(sub.hdnGrandTotal || 0), 0) || 0;
+ const orderTotal = sales_orders?.reduce((sum, order) => sum + parseFloat(order.hdnGrandTotal || 0), 0) || 0;
+ const bmcOfficeTotal = bmc_office_subscriptions?.reduce((sum, sub) => sum + parseFloat(sub.total_inkl_moms || 0), 0) || 0;
+
+ if (totalItems > 0) {
+ html += `
+
+
+
+
Sammenligning
+
+
+
+
vTiger Abonnementer
+
${subscriptions?.length || 0}
+
${subTotal.toFixed(2)} DKK
+
+
+
+
+
Γ
bne Salgsordre
+
${sales_orders?.length || 0}
+
${orderTotal.toFixed(2)} DKK
+
+
+
+
+
BMC Office Abonnementer
+
${bmc_office_subscriptions?.length || 0}
+
${bmcOfficeTotal.toFixed(2)} DKK
+
+
+
+
+
Total Værdi
+
${(subTotal + orderTotal + bmcOfficeTotal).toFixed(2)} DKK
+
Samlet omsætning
+
+
+
+
+
+
+ `;
+ }
+
+ container.innerHTML = html;
+}
+
+function renderRecurringOrdersList(orders) {
+ if (!orders || orders.length === 0) {
+ return '
Ingen tilbagevendende ordrer
';
+ }
+
+ return orders.map((order, idx) => {
+ const itemId = `recurring-${idx}`;
+ const lineItems = order.lineItems || [];
+ const hasLineItems = Array.isArray(lineItems) && lineItems.length > 0;
+
+ return `
+
+
+
+
+ ${escapeHtml(order.subject || order.salesorder_no || 'Unnamed')}
+
+
${escapeHtml(order.sostatus || 'Open')}
+
+ ${order.description ? `
${escapeHtml(order.description).substring(0, 100)}...
` : ''}
+
+ ${order.recurring_frequency ? `${escapeHtml(order.recurring_frequency)}` : ''}
+ ${order.start_period ? `${formatDate(order.start_period)}` : ''}
+ ${order.end_period ? `${formatDate(order.end_period)}` : ''}
+ ${order.hdnGrandTotal ? `${parseFloat(order.hdnGrandTotal).toFixed(2)} DKK` : ''}
+
+ ${hasLineItems ? `
+
+
+
Produktlinjer:
+ ${lineItems.map(line => `
+
+
${escapeHtml(line.product_name || line.productid)}
+
+ Antal: ${line.quantity || 0} Γ ${parseFloat(line.listprice || 0).toFixed(2)} DKK =
+ ${parseFloat(line.netprice || 0).toFixed(2)} DKK
+
+ ${line.comment ? `
${escapeHtml(line.comment)}
` : ''}
+
+ `).join('')}
+
+
+ ` : ''}
+
+ `;
+ }).join('');
+}
+
+function renderSalesOrdersList(orders) {
+ if (!orders || orders.length === 0) {
+ return '
Ingen salgsordre
';
+ }
+
+ return orders.map((order, idx) => {
+ const itemId = `salesorder-${idx}`;
+ const lineItems = order.lineItems || [];
+ const hasLineItems = Array.isArray(lineItems) && lineItems.length > 0;
+ const total = parseFloat(order.hdnGrandTotal || 0);
+
+ return `
+
+
+
+
+
+ ${escapeHtml(order.subject || order.salesorder_no || 'Unnamed')}
+
+
+ ${order.salesorder_no ? `#${escapeHtml(order.salesorder_no)}` : ''}
+
+
+
+
${escapeHtml(order.sostatus || 'Open')}
+
${total.toFixed(2)} DKK
+
+
+
+
+ ${order.recurring_frequency ? `${escapeHtml(order.recurring_frequency)}` : ''}
+
+
+ ${hasLineItems ? `
+
+
+ ${lineItems.map(line => `
+
+
+
${escapeHtml(line.product_name || line.productid)}
+
+ ${line.quantity || 0} stk Γ ${parseFloat(line.listprice || 0).toFixed(2)} DKK
+
+
+
+ ${parseFloat(line.netprice || 0).toFixed(2)} DKK
+
+
+ `).join('')}
+
+ Subtotal:
+ ${parseFloat(order.hdnSubTotal || 0).toFixed(2)} DKK
+
+
+ Total inkl. moms:
+ ${total.toFixed(2)} DKK
+
+
+
+ ` : ''}
+
+ `;
+ }).join('');
+}
+
+function renderBmcOfficeSubscriptionsList(subscriptions) {
+ if (!subscriptions || subscriptions.length === 0) {
+ return '
Ingen BMC Office abonnementer
';
+ }
+
+ return subscriptions.map((sub, idx) => {
+ const itemId = `bmcoffice-${idx}`;
+ const total = parseFloat(sub.total_inkl_moms || 0);
+ const subtotal = parseFloat(sub.subtotal || 0);
+ const rabat = parseFloat(sub.rabat || 0);
+
+ return `
+
+
+
+
+
+ ${escapeHtml(sub.text || 'Unnamed')}
+
+
+ ${sub.firma_name ? `${escapeHtml(sub.firma_name)}` : ''}
+
+
+
+
${sub.active ? 'Aktiv' : 'Inaktiv'}
+
${total.toFixed(2)} DKK
+
+
+
+
+ ${sub.start_date ? `Start: ${formatDate(sub.start_date)}` : ''}
+ ${sub.antal ? `Antal: ${sub.antal}` : ''}
+ ${sub.faktura_firma_name && sub.faktura_firma_name !== sub.firma_name ? `${escapeHtml(sub.faktura_firma_name)}` : ''}
+
+
+
+
+
+ Antal:
+ ${sub.antal}
+
+
+ Pris pr. stk:
+ ${parseFloat(sub.pris || 0).toFixed(2)} DKK
+
+ ${rabat > 0 ? `
+
+ Rabat:
+ -${rabat.toFixed(2)} DKK
+
+ ` : ''}
+ ${sub.beskrivelse ? `
+
+ ${escapeHtml(sub.beskrivelse)}
+
+ ` : ''}
+
+ Subtotal:
+ ${subtotal.toFixed(2)} DKK
+
+
+ Total inkl. moms:
+ ${total.toFixed(2)} DKK
+
+
+
+
+ `;
+ }).join('');
+}
+
+function renderSubscriptionsList(subscriptions) {
+ if (!subscriptions || subscriptions.length === 0) {
+ return '
Ingen abonnementer
';
+ }
+
+ return subscriptions.map((sub, idx) => {
+ const itemId = `subscription-${idx}`;
+ const lineItems = sub.lineItems || [];
+ const hasLineItems = Array.isArray(lineItems) && lineItems.length > 0;
+ const total = parseFloat(sub.hdnGrandTotal || 0);
+
+ return `
+
+
+
+
+
+ ${escapeHtml(sub.subject || sub.subscription_no || 'Unnamed')}
+
+
+ ${sub.subscription_no ? `#${escapeHtml(sub.subscription_no)}` : ''}
+
+
+
+
${escapeHtml(sub.subscriptionstatus || 'Active')}
+
${total.toFixed(2)} DKK
+
+
+
+
+ ${sub.generateinvoiceevery ? `${escapeHtml(sub.generateinvoiceevery)}` : ''}
+ ${sub.startdate && sub.enddate ? `${formatDate(sub.startdate)} - ${formatDate(sub.enddate)}` : ''}
+ ${sub.startdate ? `Start: ${formatDate(sub.startdate)}` : ''}
+
+
+ ${hasLineItems ? `
+
+
+ ${lineItems.map(line => `
+
+
+
${escapeHtml(line.product_name || line.productid)}
+
+ ${line.quantity || 0} stk Γ ${parseFloat(line.listprice || 0).toFixed(2)} DKK
+
+
+
+ ${parseFloat(line.netprice || 0).toFixed(2)} DKK
+
+
+ `).join('')}
+
+ Subtotal:
+ ${parseFloat(sub.hdnSubTotal || 0).toFixed(2)} DKK
+
+
+ Total inkl. moms:
+ ${total.toFixed(2)} DKK
+
+
+
+ ` : ''}
+
+ `;
+ }).join('');
+}
+
+function renderInvoicesList(invoices) {
+ if (!invoices || invoices.length === 0) {
+ return '
Ingen fakturaer
';
+ }
+
+ return invoices.map((inv, idx) => {
+ const itemId = `regular-invoice-${idx}`;
+ const lineItems = inv.lineItems || [];
+ const hasLineItems = Array.isArray(lineItems) && lineItems.length > 0;
+
+ return `
+
+
+
+
+ ${escapeHtml(inv.subject || inv.invoice_no || 'Unnamed')}
+
+
${escapeHtml(inv.invoicestatus || 'Draft')}
+
+
+ ${inv.invoicedate ? `${formatDate(inv.invoicedate)}` : ''}
+ ${inv.invoice_no ? `${escapeHtml(inv.invoice_no)}` : ''}
+ ${inv.hdnGrandTotal ? `${parseFloat(inv.hdnGrandTotal).toFixed(2)} DKK` : ''}
+
+ ${hasLineItems ? `
+
+
+
Produktlinjer:
+ ${lineItems.map(line => `
+
+
${escapeHtml(line.product_name || line.productid)}
+
+ Antal: ${line.quantity || 0} Γ ${parseFloat(line.listprice || 0).toFixed(2)} DKK =
+ ${parseFloat(line.netprice || 0).toFixed(2)} DKK
+
+ ${line.comment ? `
${escapeHtml(line.comment)}
` : ''}
+
+ `).join('')}
+
+
+ Subtotal:
+ ${parseFloat(inv.hdnSubTotal || 0).toFixed(2)} DKK
+
+
+ Total inkl. moms:
+ ${parseFloat(inv.hdnGrandTotal || 0).toFixed(2)} DKK
+
+
+
+
+ ` : ''}
+
+ `;
+ }).join('');
+}
+
+function getStatusColor(status) {
+ if (!status) return 'secondary';
+ const s = status.toLowerCase();
+ if (s.includes('active') || s.includes('approved')) return 'success';
+ if (s.includes('pending')) return 'warning';
+ if (s.includes('cancelled') || s.includes('expired')) return 'danger';
+ return 'info';
+}
+
+function formatDate(dateStr) {
+ if (!dateStr) return '';
+ try {
+ const date = new Date(dateStr);
+ return date.toLocaleDateString('da-DK', { day: '2-digit', month: '2-digit', year: 'numeric' });
+ } catch {
+ return dateStr;
+ }
+}
+
async function loadActivity() {
const container = document.getElementById('activityContainer');
container.innerHTML = '
';
@@ -468,6 +1041,28 @@ async function loadActivity() {
}, 500);
}
+function toggleLineItems(itemId) {
+ const linesDiv = document.getElementById(`${itemId}-lines`);
+ const icon = document.getElementById(`${itemId}-icon`);
+
+ if (!linesDiv) return;
+
+ const item = linesDiv.closest('.subscription-item');
+
+ if (linesDiv.style.display === 'none') {
+ linesDiv.style.display = 'block';
+ if (icon) icon.className = 'bi bi-chevron-down me-2 text-primary';
+ if (item) item.classList.add('expanded');
+ } else {
+ linesDiv.style.display = 'none';
+ if (icon) {
+ const isSubscription = itemId.includes('subscription');
+ icon.className = `bi bi-chevron-right me-2 ${isSubscription ? 'text-primary' : 'text-success'}`;
+ }
+ if (item) item.classList.remove('expanded');
+ }
+}
+
function editCustomer() {
// TODO: Open edit modal with pre-filled data
console.log('Edit customer:', customerId);
diff --git a/app/services/vtiger_service.py b/app/services/vtiger_service.py
new file mode 100644
index 0000000..b149cc3
--- /dev/null
+++ b/app/services/vtiger_service.py
@@ -0,0 +1,190 @@
+"""
+vTiger Cloud CRM Integration Service
+Handles subscription and sales order data retrieval
+"""
+import logging
+import aiohttp
+from typing import List, Dict, Optional
+from app.core.config import settings
+
+logger = logging.getLogger(__name__)
+
+
+class VTigerService:
+ """Service for integrating with vTiger Cloud CRM via REST API"""
+
+ def __init__(self):
+ self.base_url = getattr(settings, 'VTIGER_URL', None)
+ self.username = getattr(settings, 'VTIGER_USERNAME', None)
+ self.api_key = getattr(settings, 'VTIGER_API_KEY', None)
+
+ # REST API endpoint
+ if self.base_url:
+ self.rest_endpoint = f"{self.base_url}/restapi/v1/vtiger/default"
+ else:
+ self.rest_endpoint = None
+
+ if not all([self.base_url, self.username, self.api_key]):
+ logger.warning("β οΈ vTiger credentials not fully configured")
+
+ def _get_auth(self):
+ """Get HTTP Basic Auth credentials"""
+ if not self.api_key:
+ raise ValueError("VTIGER_API_KEY not configured")
+ return aiohttp.BasicAuth(self.username, self.api_key)
+
+ async def query(self, query_string: str) -> List[Dict]:
+ """
+ Execute a query on vTiger REST API
+
+ Args:
+ query_string: SQL-like query (e.g., "SELECT * FROM Accounts;")
+
+ Returns:
+ List of records
+ """
+ if not self.rest_endpoint:
+ raise ValueError("VTIGER_URL not configured")
+
+ try:
+ auth = self._get_auth()
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"{self.rest_endpoint}/query",
+ params={"query": query_string},
+ auth=auth
+ ) as response:
+ text = await response.text()
+
+ if response.status == 200:
+ # vTiger returns text/json instead of application/json
+ import json
+ try:
+ data = json.loads(text)
+ except json.JSONDecodeError as e:
+ logger.error(f"β Invalid JSON in query response: {text[:200]}")
+ return []
+
+ if data.get('success'):
+ result = data.get('result', [])
+ logger.info(f"β
Query returned {len(result)} records")
+ return result
+ else:
+ logger.error(f"β vTiger query failed: {data.get('error')}")
+ return []
+ else:
+ logger.error(f"β vTiger query HTTP error {response.status}")
+ logger.error(f"Query: {query_string}")
+ logger.error(f"Response: {text[:500]}")
+ return []
+ except Exception as e:
+ logger.error(f"β vTiger query error: {e}")
+ return []
+
+ async def get_customer_sales_orders(self, vtiger_account_id: str) -> List[Dict]:
+ """
+ Fetch sales orders for a customer from vTiger
+
+ Args:
+ vtiger_account_id: vTiger account ID (e.g., "3x760")
+
+ Returns:
+ List of sales order records
+ """
+ if not vtiger_account_id:
+ logger.warning("β οΈ No vTiger account ID provided")
+ return []
+
+ try:
+ # Query for sales orders linked to this account
+ query = f"SELECT * FROM SalesOrder WHERE account_id='{vtiger_account_id}';"
+
+ logger.info(f"π Fetching sales orders for vTiger account {vtiger_account_id}")
+ orders = await self.query(query)
+
+ logger.info(f"β
Found {len(orders)} sales orders")
+ return orders
+
+ except Exception as e:
+ logger.error(f"β Error fetching sales orders: {e}")
+ return []
+
+ async def get_customer_subscriptions(self, vtiger_account_id: str) -> List[Dict]:
+ """
+ Fetch subscriptions for a customer from vTiger
+
+ Args:
+ vtiger_account_id: vTiger account ID (e.g., "3x760")
+
+ Returns:
+ List of subscription records
+ """
+ if not vtiger_account_id:
+ logger.warning("β οΈ No vTiger account ID provided")
+ return []
+
+ try:
+ # Query for subscriptions linked to this account (note: module name is singular "Subscription")
+ query = f"SELECT * FROM Subscription WHERE account_id='{vtiger_account_id}';"
+
+ logger.info(f"π Fetching subscriptions for vTiger account {vtiger_account_id}")
+ subscriptions = await self.query(query)
+
+ logger.info(f"β
Found {len(subscriptions)} subscriptions")
+ return subscriptions
+
+ except Exception as e:
+ logger.error(f"β Error fetching subscriptions: {e}")
+ return []
+
+ async def test_connection(self) -> bool:
+ """
+ Test vTiger connection using /me endpoint
+
+ Returns:
+ True if connection successful
+ """
+ if not self.rest_endpoint:
+ raise ValueError("VTIGER_URL not configured in .env")
+
+ try:
+ auth = self._get_auth()
+ logger.info(f"π Testing vTiger connection...")
+
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"{self.rest_endpoint}/me",
+ auth=auth
+ ) as response:
+ if response.status == 200:
+ # vTiger returns text/json instead of application/json
+ text = await response.text()
+ import json
+ data = json.loads(text)
+
+ if data.get('success'):
+ user_name = data['result'].get('user_name')
+ logger.info(f"β
vTiger connection successful (user: {user_name})")
+ return True
+ else:
+ logger.error(f"β vTiger API returned success=false: {data}")
+ return False
+ else:
+ error_text = await response.text()
+ logger.error(f"β vTiger connection failed: HTTP {response.status}: {error_text}")
+ return False
+
+ except Exception as e:
+ logger.error(f"β vTiger connection error: {e}")
+ return False
+
+
+# Singleton instance
+_vtiger_service = None
+
+def get_vtiger_service() -> VTigerService:
+ """Get or create vTiger service singleton"""
+ global _vtiger_service
+ if _vtiger_service is None:
+ _vtiger_service = VTigerService()
+ return _vtiger_service
diff --git a/migrations/015_bmc_office_subscriptions.sql b/migrations/015_bmc_office_subscriptions.sql
new file mode 100644
index 0000000..b9befbd
--- /dev/null
+++ b/migrations/015_bmc_office_subscriptions.sql
@@ -0,0 +1,59 @@
+-- BMC Office Subscriptions
+-- Gemmer abonnementsdata importeret fra BMC Office system
+
+CREATE TABLE IF NOT EXISTS bmc_office_subscriptions (
+ id SERIAL PRIMARY KEY,
+ customer_id INTEGER REFERENCES customers(id) ON DELETE CASCADE,
+
+ -- BMC Office data
+ firma_id VARCHAR(50), -- FirmaID fra BMC Office
+ firma_name VARCHAR(255), -- Firma navn
+ start_date DATE, -- Startdate
+ text VARCHAR(500), -- Produkt/service beskrivelse
+ antal DECIMAL(10,2) DEFAULT 1, -- Antal
+ pris DECIMAL(10,2), -- Pris per enhed
+ rabat DECIMAL(10,2) DEFAULT 0, -- Rabat i DKK
+ beskrivelse TEXT, -- Ekstra beskrivelse/noter
+
+ -- Faktura info
+ faktura_firma_id VARCHAR(50), -- FakturaFirmaID
+ faktura_firma_name VARCHAR(255), -- Fakturafirma navn
+
+ -- Status
+ active BOOLEAN DEFAULT TRUE,
+
+ -- Metadata
+ imported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ deleted_at TIMESTAMP
+);
+
+-- Indexes for performance
+CREATE INDEX IF NOT EXISTS idx_bmc_office_subs_customer ON bmc_office_subscriptions(customer_id);
+CREATE INDEX IF NOT EXISTS idx_bmc_office_subs_firma_id ON bmc_office_subscriptions(firma_id);
+CREATE INDEX IF NOT EXISTS idx_bmc_office_subs_faktura_firma_id ON bmc_office_subscriptions(faktura_firma_id);
+CREATE INDEX IF NOT EXISTS idx_bmc_office_subs_active ON bmc_office_subscriptions(active);
+
+-- View for calculating totals
+CREATE OR REPLACE VIEW bmc_office_subscription_totals AS
+SELECT
+ id,
+ customer_id,
+ firma_id,
+ firma_name,
+ text,
+ antal,
+ pris,
+ rabat,
+ (antal * pris) - rabat AS subtotal,
+ ((antal * pris) - rabat) * 1.25 AS total_inkl_moms,
+ start_date,
+ beskrivelse,
+ faktura_firma_name,
+ active
+FROM bmc_office_subscriptions
+WHERE deleted_at IS NULL;
+
+COMMENT ON TABLE bmc_office_subscriptions IS 'Abonnementer importeret fra BMC Office legacy system';
+COMMENT ON VIEW bmc_office_subscription_totals IS 'Beregner subtotal og total inkl. moms for BMC Office abonnementer';
diff --git a/test_subscription_singular.py b/test_subscription_singular.py
new file mode 100644
index 0000000..c7ce2c5
--- /dev/null
+++ b/test_subscription_singular.py
@@ -0,0 +1,34 @@
+import asyncio
+import aiohttp
+import json
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+async def test_vtiger():
+ base_url = os.getenv('VTIGER_URL')
+ username = os.getenv('VTIGER_USERNAME')
+ api_key = os.getenv('VTIGER_API_KEY')
+
+ auth = aiohttp.BasicAuth(username, api_key)
+
+ # Test query with singular "Subscription"
+ vtiger_id = '3x760'
+ query = f"SELECT * FROM Subscription WHERE account_id='{vtiger_id}';"
+ print(f"π Testing query: {query}")
+
+ async with aiohttp.ClientSession() as session:
+ async with session.get(
+ f"{base_url}/restapi/v1/vtiger/default/query",
+ params={"query": query},
+ auth=auth
+ ) as response:
+ text = await response.text()
+ print(f"Status: {response.status}")
+ print(f"Response: {text[:500]}")
+ if response.status == 200:
+ data = json.loads(text)
+ print(json.dumps(data, indent=2))
+
+asyncio.run(test_vtiger())
diff --git a/test_subscriptions.py b/test_subscriptions.py
new file mode 100644
index 0000000..188244c
--- /dev/null
+++ b/test_subscriptions.py
@@ -0,0 +1,66 @@
+import asyncio
+import aiohttp
+import json
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+async def test_vtiger():
+ base_url = os.getenv('VTIGER_URL')
+ username = os.getenv('VTIGER_USERNAME')
+ api_key = os.getenv('VTIGER_API_KEY')
+
+ print(f"π Testing vTiger connection...")
+ print(f"URL: {base_url}")
+ print(f"Username: {username}")
+
+ auth = aiohttp.BasicAuth(username, api_key)
+
+ # Test 1: Connection
+ async with aiohttp.ClientSession() as session:
+ async with session.get(f"{base_url}/restapi/v1/vtiger/default/me", auth=auth) as response:
+ text = await response.text()
+ print(f"\nβ
Connection test: {response.status}")
+ data = json.loads(text)
+ print(json.dumps(data, indent=2))
+
+ # Test 2: List all modules
+ async with session.get(f"{base_url}/restapi/v1/vtiger/default/listtypes", auth=auth) as response:
+ text = await response.text()
+ data = json.loads(text)
+ if data.get('success'):
+ modules = data.get('result', {}).get('types', [])
+ print(f"\nπ Available modules ({len(modules)}):")
+ for mod in sorted(modules):
+ if 'sub' in mod.lower() or 'invoice' in mod.lower() or 'order' in mod.lower():
+ print(f" - {mod}")
+
+ # Test 3: Query Subscriptions
+ vtiger_id = '3x760'
+ query = f"SELECT * FROM Subscriptions WHERE account_id='{vtiger_id}';"
+ print(f"\nπ Testing query: {query}")
+ async with session.get(
+ f"{base_url}/restapi/v1/vtiger/default/query",
+ params={"query": query},
+ auth=auth
+ ) as response:
+ text = await response.text()
+ print(f"Status: {response.status}")
+ data = json.loads(text)
+ print(json.dumps(data, indent=2))
+
+ # Test 4: Query Invoice
+ query2 = f"SELECT * FROM Invoice WHERE account_id='{vtiger_id}';"
+ print(f"\nπ Testing Invoice query: {query2}")
+ async with session.get(
+ f"{base_url}/restapi/v1/vtiger/default/query",
+ params={"query": query2},
+ auth=auth
+ ) as response:
+ text = await response.text()
+ print(f"Status: {response.status}")
+ data = json.loads(text)
+ print(json.dumps(data, indent=2))
+
+asyncio.run(test_vtiger())
diff --git a/test_vtiger_fields.py b/test_vtiger_fields.py
new file mode 100644
index 0000000..0f1a1ec
--- /dev/null
+++ b/test_vtiger_fields.py
@@ -0,0 +1,60 @@
+"""
+Detailed vTiger field inspection for SalesOrder
+"""
+import asyncio
+import sys
+import json
+sys.path.insert(0, '/app')
+
+from app.services.vtiger_service import get_vtiger_service
+
+async def inspect_fields():
+ vtiger = get_vtiger_service()
+
+ print("="*60)
+ print("Inspecting SalesOrder for Arbodania (3x760)")
+ print("="*60)
+
+ query = "SELECT * FROM SalesOrder WHERE account_id='3x760';"
+ results = await vtiger.query(query)
+
+ if results:
+ print(f"\nβ
Found {len(results)} sales orders\n")
+ for i, order in enumerate(results, 1):
+ print(f"\n{'='*60}")
+ print(f"Sales Order #{i}")
+ print(f"{'='*60}")
+ for key, value in sorted(order.items()):
+ if value and str(value).strip(): # Only show non-empty values
+ print(f"{key:30s} = {value}")
+
+ print("\n" + "="*60)
+ print("Inspecting ALL SalesOrders (first 5)")
+ print("="*60)
+
+ query2 = "SELECT * FROM SalesOrder LIMIT 5;"
+ all_orders = await vtiger.query(query2)
+
+ if all_orders:
+ print(f"\nβ
Found {len(all_orders)} sales orders total\n")
+
+ # Collect all unique field names
+ all_fields = set()
+ for order in all_orders:
+ all_fields.update(order.keys())
+
+ print(f"Total unique fields: {len(all_fields)}")
+ print("\nField names related to frequency/recurring:")
+ freq_fields = [f for f in sorted(all_fields) if any(x in f.lower() for x in ['freq', 'recur', 'billing', 'period', 'subscr'])]
+ if freq_fields:
+ for f in freq_fields:
+ print(f" - {f}")
+ else:
+ print(" β οΈ No frequency-related fields found")
+
+ print("\nAll field names:")
+ for f in sorted(all_fields):
+ print(f" - {f}")
+
+if __name__ == "__main__":
+ asyncio.run(inspect_fields())
diff --git a/test_vtiger_modules.py b/test_vtiger_modules.py
new file mode 100644
index 0000000..c2f07b3
--- /dev/null
+++ b/test_vtiger_modules.py
@@ -0,0 +1,58 @@
+"""
+Test vTiger modules and queries
+"""
+import asyncio
+import sys
+sys.path.insert(0, '/app')
+
+from app.services.vtiger_service import get_vtiger_service
+
+async def test_vtiger():
+ vtiger = get_vtiger_service()
+
+ # Test connection
+ print("π Testing vTiger connection...")
+ connected = await vtiger.test_connection()
+ if not connected:
+ print("β Connection failed!")
+ return
+
+ print("\n" + "="*60)
+ print("Testing different module queries for account 3x760")
+ print("="*60)
+
+ # Test various queries
+ queries = [
+ # Try different module names
+ ("Services", "SELECT * FROM Services WHERE account_id='3x760' LIMIT 5;"),
+ ("Products", "SELECT * FROM Products WHERE account_id='3x760' LIMIT 5;"),
+ ("SalesOrder", "SELECT * FROM SalesOrder WHERE account_id='3x760' LIMIT 5;"),
+ ("Invoice", "SELECT * FROM Invoice WHERE account_id='3x760' LIMIT 5;"),
+ ("Quotes", "SELECT * FROM Quotes WHERE account_id='3x760' LIMIT 5;"),
+ ("Contacts", "SELECT * FROM Contacts WHERE account_id='3x760' LIMIT 5;"),
+
+ # Try without account filter to see structure
+ ("SalesOrder (all)", "SELECT * FROM SalesOrder LIMIT 2;"),
+ ("Invoice (all)", "SELECT * FROM Invoice LIMIT 2;"),
+ ]
+
+ for name, query in queries:
+ print(f"\nπ Testing: {name}")
+ print(f"Query: {query}")
+ try:
+ results = await vtiger.query(query)
+ if results:
+ print(f"β
Found {len(results)} records")
+ if len(results) > 0:
+ print(f"Sample keys: {list(results[0].keys())[:10]}")
+ # Show first record
+ print("\nFirst record:")
+ for key, value in list(results[0].items())[:15]:
+ print(f" {key}: {value}")
+ else:
+ print("β οΈ No results")
+ except Exception as e:
+ print(f"β Error: {e}")
+
+if __name__ == "__main__":
+ asyncio.run(test_vtiger())