From 361f2fad5ddd4bf6eda5892c02d150eb715cc000 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 11 Dec 2025 23:14:20 +0100 Subject: [PATCH] feat: Implement vTiger integration for subscriptions and sales orders - Added a new VTigerService class for handling API interactions with vTiger CRM. - Implemented methods to fetch customer subscriptions and sales orders. - Created a new database migration for BMC Office subscriptions, including table structure and view for totals. - Enhanced customer detail frontend to display subscriptions and sales orders with improved UI/UX. - Added JavaScript functions for loading and displaying subscription data dynamically. - Created tests for vTiger API queries and field inspections to ensure data integrity and functionality. --- app/customers/backend/router.py | 105 ++++ app/customers/frontend/customer_detail.html | 607 +++++++++++++++++++- app/services/vtiger_service.py | 190 ++++++ migrations/015_bmc_office_subscriptions.sql | 59 ++ test_subscription_singular.py | 34 ++ test_subscriptions.py | 66 +++ test_vtiger_fields.py | 60 ++ test_vtiger_modules.py | 58 ++ 8 files changed, 1173 insertions(+), 6 deletions(-) create mode 100644 app/services/vtiger_service.py create mode 100644 migrations/015_bmc_office_subscriptions.sql create mode 100644 test_subscription_singular.py create mode 100644 test_subscriptions.py create mode 100644 test_vtiger_fields.py create mode 100644 test_vtiger_modules.py diff --git a/app/customers/backend/router.py b/app/customers/backend/router.py index 02d47f4..8826755 100644 --- a/app/customers/backend/router.py +++ b/app/customers/backend/router.py @@ -355,3 +355,108 @@ async def lookup_cvr(cvr_number: str): raise HTTPException(status_code=404, detail="CVR number not found") return result + + +@router.get("/customers/{customer_id}/subscriptions") +async def get_customer_subscriptions(customer_id: int): + """ + Get subscriptions and sales orders for a customer + Returns data from vTiger: + 1. Recurring Sales Orders (enable_recurring = 1) + 2. Sales Orders with recurring_frequency (open status) + 3. Recent Invoices for context + """ + from app.services.vtiger_service import get_vtiger_service + + # Get customer with vTiger ID + customer = execute_query( + "SELECT id, name, vtiger_id FROM customers WHERE id = %s", + (customer_id,), + fetchone=True + ) + + if not customer: + raise HTTPException(status_code=404, detail="Customer not found") + + vtiger_id = customer.get('vtiger_id') + + if not vtiger_id: + logger.warning(f"⚠️ Customer {customer_id} has no vTiger ID") + return { + "status": "no_vtiger_link", + "message": "Kunde er ikke synkroniseret med vTiger", + "recurring_orders": [], + "sales_orders": [], + "invoices": [] + } + + try: + vtiger = get_vtiger_service() + + # Fetch all sales orders + logger.info(f"πŸ” Fetching sales orders for vTiger account {vtiger_id}") + all_orders = await vtiger.get_customer_sales_orders(vtiger_id) + + # Fetch subscriptions from vTiger + logger.info(f"πŸ” Fetching subscriptions for vTiger account {vtiger_id}") + subscriptions = await vtiger.get_customer_subscriptions(vtiger_id) + + # Filter sales orders into categories + recurring_orders = [] + frequency_orders = [] + all_open_orders = [] + + for order in all_orders: + # Skip closed/cancelled orders + status = order.get('sostatus', '').lower() + if status in ['closed', 'cancelled']: + continue + + all_open_orders.append(order) + + # Check if recurring is enabled + enable_recurring = order.get('enable_recurring') + recurring_frequency = order.get('recurring_frequency', '').strip() + + if enable_recurring == '1' or enable_recurring == 1: + recurring_orders.append(order) + elif recurring_frequency: + frequency_orders.append(order) + + # Filter subscriptions by status + active_subscriptions = [] + expired_subscriptions = [] + + for sub in subscriptions: + status = sub.get('sub_status', '').lower() + if status in ['cancelled', 'expired']: + expired_subscriptions.append(sub) + else: + active_subscriptions.append(sub) + + # Fetch BMC Office subscriptions from local database + bmc_office_query = """ + SELECT * FROM bmc_office_subscription_totals + WHERE customer_id = %s AND active = true + ORDER BY start_date DESC + """ + bmc_office_subs = execute_query(bmc_office_query, (customer_id,)) or [] + + logger.info(f"βœ… Found {len(recurring_orders)} recurring orders, {len(frequency_orders)} frequency orders, {len(all_open_orders)} total open orders, {len(active_subscriptions)} active subscriptions, {len(bmc_office_subs)} BMC Office subscriptions") + + return { + "status": "success", + "customer_id": customer_id, + "customer_name": customer['name'], + "vtiger_id": vtiger_id, + "recurring_orders": recurring_orders, + "sales_orders": all_open_orders, # Show ALL open sales orders + "subscriptions": active_subscriptions, # Active subscriptions from vTiger Subscriptions module + "expired_subscriptions": expired_subscriptions, + "bmc_office_subscriptions": bmc_office_subs, # Local BMC Office subscriptions + "last_updated": "real-time" + } + + except Exception as e: + logger.error(f"❌ Error fetching subscriptions: {e}") + raise HTTPException(status_code=500, detail=f"Failed to fetch subscriptions: {str(e)}") diff --git a/app/customers/frontend/customer_detail.html b/app/customers/frontend/customer_detail.html index 46a4c00..c5bfa46 100644 --- a/app/customers/frontend/customer_detail.html +++ b/app/customers/frontend/customer_detail.html @@ -105,6 +105,53 @@ position: relative; } + .subscription-item { + transition: all 0.3s ease; + cursor: pointer; + } + + .subscription-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12) !important; + } + + .subscription-item.expanded { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15) !important; + border-color: var(--accent) !important; + } + + .subscription-column { + min-height: 200px; + } + + .column-header { + position: sticky; + top: 0; + z-index: 10; + background: white; + padding: 1rem; + margin: -1rem -1rem 1rem -1rem; + border-radius: 8px 8px 0 0; + } + + .line-item-details { + background: rgba(0, 0, 0, 0.02); + border-radius: 6px; + padding: 0.5rem; + } + + .expandable-item { + transition: all 0.2s; + } + + .expandable-item:hover { + background: rgba(0, 0, 0, 0.02); + } + + .chevron-icon { + transition: transform 0.2s; + } + .activity-item::before { content: ''; position: absolute; @@ -165,6 +212,11 @@ Fakturaer +